diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java index 42602e8429..cd3eb50740 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.instructions; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.TbVersionUtils; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EdgeUpgradeInfo; @@ -30,6 +31,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -62,7 +64,11 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad } @Override - public void updateInstructionMap(Map map) { + public void updateVersionGraph(Map> versionGraph) { + updateInstructionMap(EdgeVersionGraphResolver.resolve(versionGraph, platformEdgeVersion)); + } + + private void updateInstructionMap(Map map) { for (String key : map.keySet()) { upgradeVersionHashMap.put(key, map.get(key)); } @@ -73,29 +79,13 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad Optional attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get(); if (attributeKvEntryOpt.isPresent()) { String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString()); - return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, platformEdgeVersion); + String platformEdgeVersionFormatted = TbVersionUtils.extractStartingDigits(platformEdgeVersion); + return TbVersionUtils.compare(edgeVersionFormatted, "3.6.0") >= 0 + && TbVersionUtils.compare(edgeVersionFormatted, platformEdgeVersionFormatted) < 0; } return false; } - private boolean isVersionGreaterOrEqualsThan(String version1, String version2) { - String[] v1 = version1.split("\\."); - String[] v2 = version2.split("\\."); - - int length = Math.max(v1.length, v2.length); - for (int i = 0; i < length; i++) { - int num1 = i < v1.length ? Integer.parseInt(v1[i]) : 0; - int num2 = i < v2.length ? Integer.parseInt(v2[i]) : 0; - - if (num1 < num2) { - return false; - } else if (num1 > num2) { - return true; - } - } - return true; - } - private EdgeInstructions getDockerUpgradeInstructions(String platformEdgeVersion, String currentEdgeVersion) { EdgeUpgradeInfo edgeUpgradeInfo = upgradeVersionHashMap.get(currentEdgeVersion); if (edgeUpgradeInfo == null || edgeUpgradeInfo.getNextEdgeVersion() == null || platformEdgeVersion.equals(currentEdgeVersion)) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java index f56a9a3047..1546c588ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java @@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.List; import java.util.Map; public interface EdgeUpgradeInstructionsService { EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod); - void updateInstructionMap(Map upgradeVersions); + void updateVersionGraph(Map> versionGraph); void setPlatformEdgeVersion(String version); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeVersionGraphResolver.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeVersionGraphResolver.java new file mode 100644 index 0000000000..f650b1e834 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeVersionGraphResolver.java @@ -0,0 +1,56 @@ +/** + * 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.service.edge.instructions; + +import org.thingsboard.common.util.TbVersionUtils; +import org.thingsboard.server.common.data.EdgeUpgradeInfo; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class EdgeVersionGraphResolver { + + private EdgeVersionGraphResolver() { + } + + /** + * Collapses a branching upgrade version graph into a single upgrade option per source version. + * For each version the option with the highest {@code nextEdgeVersion} that is lower than or equal to the + * given platform version is kept; if every option points to a newer version the source version becomes + * terminal ({@code nextEdgeVersion == null}). + */ + public static Map resolve(Map> versionGraph, String platformVersion) { + String platform = TbVersionUtils.extractStartingDigits(platformVersion); + Map resolved = new HashMap<>(); + for (var entry : versionGraph.entrySet()) { + EdgeUpgradeInfo best = null; + for (EdgeUpgradeInfo option : entry.getValue()) { + String next = option.getNextEdgeVersion(); + // eligible only if next is present and lower than or equal to the platform version + if (next == null || TbVersionUtils.compare(next, platform) > 0) { + continue; + } + // keep the option with the highest eligible nextEdgeVersion + if (best == null || TbVersionUtils.compare(next, best.getNextEdgeVersion()) >= 0) { + best = option; + } + } + resolved.put(entry.getKey(), best != null ? best : new EdgeUpgradeInfo(false, null)); + } + return resolved; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index e31a20092a..e7cb71326e 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -28,7 +28,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.data.EdgeUpgradeMessage; +import org.thingsboard.server.common.data.EdgeUpgradeMessageV2; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; @@ -150,14 +150,14 @@ public class DefaultUpdateService implements UpdateService { .build()); } ObjectNode edgeRequest = JacksonUtil.newObjectNode().put(VERSION_PARAM, version); - String edgePlatformVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); + String edgePlatformVersion = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v2/edge/installMapping", new HttpEntity<>(edgeRequest.toString(), headers), String.class); if (edgePlatformVersion != null) { edgeInstallInstructionsService.setPlatformEdgeVersion(edgePlatformVersion); edgeUpgradeInstructionsService.setPlatformEdgeVersion(edgePlatformVersion); } - EdgeUpgradeMessage edgeUpgradeMessage = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/v1/edge/upgradeMapping", new HttpEntity<>(edgeRequest.toString(), headers), EdgeUpgradeMessage.class); + EdgeUpgradeMessageV2 edgeUpgradeMessage = restClient.getForObject(UPDATE_SERVER_BASE_URL + "/api/v2/edge/upgradeMapping", EdgeUpgradeMessageV2.class); if (edgeUpgradeMessage != null) { - edgeUpgradeInstructionsService.updateInstructionMap(edgeUpgradeMessage.getEdgeVersions()); + edgeUpgradeInstructionsService.updateVersionGraph(edgeUpgradeMessage.getEdgeVersions()); } } catch (Exception e) { log.trace(e.getMessage()); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 9d50e3a62c..60bd6658b9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -1287,12 +1287,16 @@ public class EdgeControllerTest extends AbstractControllerTest { @Test public void testGetEdgeUpgradeInstructions() throws Exception { - // UpdateInfo config is updating from the Thingsboard Update server - HashMap upgradeInfoHashMap = new HashMap<>(); - upgradeInfoHashMap.put("3.6.0", new EdgeUpgradeInfo(true, "3.6.1")); - upgradeInfoHashMap.put("3.6.1", new EdgeUpgradeInfo(true, "3.6.2")); - upgradeInfoHashMap.put("3.6.2", new EdgeUpgradeInfo(true, null)); - edgeUpgradeInstructionsService.updateInstructionMap(upgradeInfoHashMap); + // Version graph is fetched from the Thingsboard Update server and resolved against the platform edge version + HashMap> versionGraph = new HashMap<>(); + versionGraph.put("3.6.0", List.of(new EdgeUpgradeInfo(true, "3.6.1"))); + versionGraph.put("3.6.1", List.of(new EdgeUpgradeInfo(true, "3.6.2"))); + // branch: resolver must keep the highest nextEdgeVersion <= platform version (3.6.4), not the newer 3.7.0 line + versionGraph.put("3.6.2", List.of(new EdgeUpgradeInfo(false, "3.6.4"), new EdgeUpgradeInfo(true, "3.7.0"))); + versionGraph.put("3.6.4", List.of(new EdgeUpgradeInfo(true, null))); + versionGraph.put("3.7.0", List.of(new EdgeUpgradeInfo(true, null))); + edgeUpgradeInstructionsService.setPlatformEdgeVersion("3.6.4"); + edgeUpgradeInstructionsService.updateVersionGraph(versionGraph); Edge edge = constructEdge("Edge for Test Docker Upgrade Instructions", "default"); Edge savedEdge = doPost("/api/edge", edge, Edge.class); String body = "{\"edgeVersion\": \"V_3_6_0\"}"; @@ -1300,6 +1304,9 @@ public class EdgeControllerTest extends AbstractControllerTest { String upgradeInstructions = doGet("/api/edge/instructions/upgrade/" + EdgeVersion.V_3_6_0.name() + "/docker", String.class); Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.1EDGE")); Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.2EDGE")); + Assert.assertTrue(upgradeInstructions.contains("Upgrading to 3.6.4EDGE")); + // 3.7.0 is newer than the platform edge version, so the resolver must not include it in the path + Assert.assertFalse(upgradeInstructions.contains("3.7.0EDGE")); } @Test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessageV2.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessageV2.java new file mode 100644 index 0000000000..77bcb43b8a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUpgradeMessageV2.java @@ -0,0 +1,33 @@ +/** + * 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.common.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +@Data +@Schema +public class EdgeUpgradeMessageV2 implements Serializable { + + private static final long serialVersionUID = -6647214126993298281L; + + @Schema(description = "Mapping of edge version to the list of available upgrade options (next ver + strategy).") + private final Map> edgeVersions; +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/TbVersionUtils.java b/common/util/src/main/java/org/thingsboard/common/util/TbVersionUtils.java new file mode 100644 index 0000000000..460515e7e4 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/TbVersionUtils.java @@ -0,0 +1,55 @@ +/** + * 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.common.util; + +public class TbVersionUtils { + + private TbVersionUtils() { + } + + /** + * Compares two dot-separated numeric version strings component-wise, treating missing trailing components as + * zero (so {@code "3.6"} equals {@code "3.6.0"}). A {@code null} or empty string is treated as the lowest version. + * + * @return a negative int, zero, or a positive int as {@code v1} is less than, equal to, or greater than {@code v2} + */ + public static int compare(String v1, String v2) { + String[] parts1 = split(v1); + String[] parts2 = split(v2); + int length = Math.max(parts1.length, parts2.length); + for (int i = 0; i < length; i++) { + int num1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0; + int num2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0; + if (num1 != num2) { + return Integer.compare(num1, num2); + } + } + return 0; + } + + /** + * Strips any suffix that follows the leading dotted-numeric part of a version string + * (e.g. {@code "4.3.1.1PE-SNAPSHOT"} -> {@code "4.3.1.1"}). Returns an empty string for a {@code null} input. + */ + public static String extractStartingDigits(String version) { + return version == null ? "" : version.replaceAll("[^0-9.].*$", ""); + } + + private static String[] split(String version) { + return version == null || version.isEmpty() ? new String[0] : version.split("\\."); + } + +}