This is an automated email from the ASF dual-hosted git repository. mpochatkin pushed a commit to branch IGNITE-27200 in repository https://gitbox.apache.org/repos/asf/ignite-3.git
commit 1b89cf845d9899ad42b9dcb7942230ba31e1bbcf Author: Pochatkin Mikhail <[email protected]> AuthorDate: Wed Dec 17 16:53:30 2025 +0300 IGNITE-27200 Add REST for access deployment structure --- .../ignite/internal/deployment/DeployFiles.java | 3 +- .../apache/ignite/internal/deployment/Unit.java | 43 ++++- .../internal/deployunit/DeploymentManagerImpl.java | 15 ++ .../internal/deployunit/FileDeployerService.java | 47 +++++ .../internal/deployunit/IgniteDeployment.java | 9 + .../internal/deployunit/UnitStructureBuilder.java | 96 +++++++++++ .../internal/deployunit/structure/UnitEntry.java | 48 ++++++ .../internal/deployunit/structure/UnitFile.java | 69 ++++++++ .../internal/deployunit/structure/UnitFolder.java | 92 ++++++++++ .../ignite/deployment/FileDeployerServiceTest.java | 34 ++++ .../compute/util/DummyIgniteDeployment.java | 6 + .../internal/testframework/IgniteTestUtils.java | 2 +- .../rest/api/deployment/DeploymentCodeApi.java | 25 +++ .../internal/rest/api/deployment/UnitEntry.java | 189 +++++++++++++++++++++ .../DeploymentManagementControllerTest.java | 48 ++++++ .../deployment/DeploymentManagementController.java | 25 +++ 16 files changed, 743 insertions(+), 8 deletions(-) diff --git a/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/DeployFiles.java b/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/DeployFiles.java index bf0424489b4..0dc7f914667 100644 --- a/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/DeployFiles.java +++ b/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/DeployFiles.java @@ -171,10 +171,11 @@ class DeployFiles { assertThat(deploy, willBe(true)); - Unit unit = new Unit(entryNode, workDir, id, version, files); + Unit unit = new Unit(entryNode, id, version, files); for (DeployFile file : files) { unit.verify(file, entryNode); + unit.verifyByRest(entryNode); } return unit; diff --git a/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/Unit.java b/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/Unit.java index c2ae55763cf..2aa0acd74e4 100644 --- a/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/Unit.java +++ b/modules/code-deployment/src/integrationTest/java/org/apache/ignite/internal/deployment/Unit.java @@ -18,8 +18,10 @@ package org.apache.ignite.internal.deployment; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -33,7 +35,9 @@ import java.util.zip.ZipInputStream; import org.apache.ignite.deployment.version.Version; import org.apache.ignite.internal.app.IgniteImpl; import org.apache.ignite.internal.deployunit.configuration.DeploymentExtensionConfiguration; -import org.hamcrest.Matchers; +import org.apache.ignite.internal.deployunit.structure.UnitEntry; +import org.apache.ignite.internal.deployunit.structure.UnitFile; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; class Unit { private final IgniteImpl deployedNode; @@ -44,11 +48,8 @@ class Unit { private final List<DeployFile> files; - private final Path workDir; - - Unit(IgniteImpl deployedNode, Path workDir, String id, Version version, List<DeployFile> files) { + Unit(IgniteImpl deployedNode, String id, Version version, List<DeployFile> files) { this.deployedNode = deployedNode; - this.workDir = workDir; this.id = id; this.version = version; this.files = files; @@ -143,10 +144,40 @@ class Unit { try { Path filePath = nodeUnitDirectory.resolve(file.file().getFileName()); assertTrue(Files.exists(filePath)); - assertThat(Files.size(filePath), Matchers.is(file.expectedSize())); + assertThat(Files.size(filePath), is(file.expectedSize())); + } catch (IOException e) { + fail(e); + } + } + } + + void verifyByRest(IgniteImpl entryNode) { + Path currentDir = getNodeUnitDirectory(entryNode); + + CompletableFuture<Void> result = entryNode.deployment().nodeUnitFileStructure(id, version).thenAccept(folder -> { + try { + processEntry(folder, currentDir); } catch (IOException e) { fail(e); } + }); + + assertThat(result, willCompleteSuccessfully()); + } + + private static void processEntry(UnitEntry entry, Path currentDir) throws IOException { + if (entry instanceof UnitFile) { + UnitFile file = (UnitFile) entry; + Path filePath = currentDir.resolve(file.name()); + assertTrue(Files.exists(filePath)); + assertThat(Files.size(filePath), is(file.size())); + } else if (entry instanceof UnitFolder) { + Path dir = currentDir.resolve(entry.name()); + for (UnitEntry child : ((UnitFolder) entry).children()) { + processEntry(child, dir); + } + } else { + fail(new IllegalStateException("Unit entry type not supported.")); } } } diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java index bd7ac3c8620..02260ee87ec 100644 --- a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/DeploymentManagerImpl.java @@ -56,6 +56,7 @@ import org.apache.ignite.internal.deployunit.metastore.NodeEventCallback; import org.apache.ignite.internal.deployunit.metastore.NodeStatusWatchListener; import org.apache.ignite.internal.deployunit.metastore.status.UnitClusterStatus; import org.apache.ignite.internal.deployunit.metastore.status.UnitNodeStatus; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; import org.apache.ignite.internal.deployunit.tempstorage.TempStorageProvider; import org.apache.ignite.internal.logger.IgniteLogger; import org.apache.ignite.internal.logger.Loggers; @@ -354,6 +355,20 @@ public class DeploymentManagerImpl implements IgniteDeployment { .thenApply(DeploymentManagerImpl::extractDeploymentStatus); } + @Override + public CompletableFuture<UnitFolder> nodeUnitFileStructure(String id, Version version) { + checkId(id); + Objects.requireNonNull(version); + + return deploymentUnitStore.getNodeStatus(nodeName, id, version).thenCompose(nodeStatus -> { + if (nodeStatus == null) { + return failedFuture(new DeploymentUnitNotFoundException(id, version)); + } + + return deployer.getUnitStructure(id, version); + }); + } + @Override public CompletableFuture<Boolean> onDemandDeploy(String id, Version version) { return deploymentUnitStore.getAllNodeStatuses(id, version) diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/FileDeployerService.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/FileDeployerService.java index 63aa198369f..7db02b6acdb 100644 --- a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/FileDeployerService.java +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/FileDeployerService.java @@ -33,6 +33,7 @@ import java.util.concurrent.Executors; import org.apache.ignite.deployment.version.Version; import org.apache.ignite.internal.deployunit.DeployerProcessor.DeployArg; import org.apache.ignite.internal.deployunit.exception.DeploymentUnitNotFoundException; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; import org.apache.ignite.internal.deployunit.tempstorage.TempStorage; import org.apache.ignite.internal.deployunit.tempstorage.TempStorageProvider; import org.apache.ignite.internal.deployunit.tempstorage.TempStorageProviderImpl; @@ -40,6 +41,7 @@ import org.apache.ignite.internal.logger.IgniteLogger; import org.apache.ignite.internal.logger.Loggers; import org.apache.ignite.internal.thread.IgniteThreadFactory; import org.apache.ignite.internal.util.IgniteUtils; +import org.jetbrains.annotations.Nullable; /** * Service for file deploying on local File System. @@ -144,6 +146,51 @@ public class FileDeployerService { }, executor); } + /** + * Returns unit content representation with tree structure. + * + * @param id Unit identifier. + * @param version Unit version. + */ + public CompletableFuture<UnitFolder> getUnitStructure(String id, Version version) { + return CompletableFuture.supplyAsync(() -> { + try { + UnitStructureBuilder builder = new UnitStructureBuilder(); + Path unitFolder = unitPath(id, version); + builder.pushFolder(id + "-" + version); + + Files.walkFileTree(unitFolder, new SimpleFileVisitor<>() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (!Files.isSameFile(dir, unitFolder)) { + builder.pushFolder(dir.getFileName().toString()); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + builder.addFile(file.getFileName().toString(), attrs.size()); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, @Nullable IOException exc) throws IOException { + if (!Files.isSameFile(dir, unitFolder)) { + builder.popFolder(dir.getFileName().toString()); + } + return FileVisitResult.CONTINUE; + } + }); + return builder.build(); + } catch (IOException e) { + LOG.error("Failed to get content for unit " + id + ":" + version, e); + return null; + } + }, executor); + } + /** * Returns path to unit folder. * diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/IgniteDeployment.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/IgniteDeployment.java index 3e96298bad2..dfdff2d0624 100644 --- a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/IgniteDeployment.java +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/IgniteDeployment.java @@ -20,6 +20,7 @@ package org.apache.ignite.internal.deployunit; import java.util.List; import java.util.concurrent.CompletableFuture; import org.apache.ignite.deployment.version.Version; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; import org.apache.ignite.internal.manager.IgniteComponent; /** @@ -149,4 +150,12 @@ public interface IgniteDeployment extends IgniteComponent { * @return Future with the latest version of the deployment unit. */ CompletableFuture<Version> detectLatestDeployedVersion(String id); + + /** + * Returns unit content tree structure. + * + * @param unitId Deployment unit identifier. + * @param version Deployment unit version. + */ + CompletableFuture<UnitFolder> nodeUnitFileStructure(String unitId, Version version); } diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitStructureBuilder.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitStructureBuilder.java new file mode 100644 index 00000000000..0fcd5c5b2c0 --- /dev/null +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/UnitStructureBuilder.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.ignite.internal.deployunit; + +import java.util.ArrayDeque; +import java.util.Deque; +import org.apache.ignite.internal.deployunit.structure.UnitFile; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; + +/** + * Builder for constructing hierarchical deployment unit structures. + * + * <p>This class uses a stack-based approach to build a tree structure of folders and files + * representing the content of a deployment unit. Folders can be nested by pushing and popping + * them on the internal stack, while files are added to the current (top) folder. + * + * @see UnitFolder + * @see UnitFile + */ +public class UnitStructureBuilder { + /** Stack of folders used to build the hierarchical structure. */ + private final Deque<UnitFolder> folderStack = new ArrayDeque<>(); + + /** + * Pushes a new folder onto the stack and adds it as a child to the current folder. + * + * <p>If there is already a folder on the stack, the new folder is added as a child + * to it. The new folder becomes the current folder (top of the stack) to which + * subsequent files and folders will be added. + * + * @param folderName the name of the folder to push + */ + public void pushFolder(String folderName) { + UnitFolder folder = folderStack.peek(); + UnitFolder newFolder = new UnitFolder(folderName); + if (folder != null) { + folder.addChild(newFolder); + } + folderStack.push(newFolder); + } + + /** + * Pops the current folder from the stack. + * + * <p>The provided folder name is verified against the name of the folder being popped + * to ensure correct nesting structure. + * + * @param folderName the expected name of the folder being popped (for verification) + */ + public void popFolder(String folderName) { + UnitFolder lastFolder = folderStack.pop(); + assert lastFolder.name().equals(folderName); + } + + /** + * Adds a file to the current folder. + * + * <p>The file is added as a child to the folder at the top of the stack. + * + * @param fileName the name of the file + * @param size the size of the file in bytes + */ + public void addFile(String fileName, long size) { + UnitFolder folder = folderStack.peek(); + folder.addChild(new UnitFile(fileName, size)); + } + + /** + * Builds and returns the root folder structure. + * + * <p>This method pops and returns the last remaining folder from the stack, + * which should be the root of the built structure. The stack should contain + * exactly one folder when this method is called. + * + * @return the root folder of the built structure + * @throws java.util.NoSuchElementException if the stack is empty + */ + public UnitFolder build() { + return folderStack.pop(); + } +} diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitEntry.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitEntry.java new file mode 100644 index 00000000000..134f6044d4c --- /dev/null +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitEntry.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.ignite.internal.deployunit.structure; + +/** + * Represents an entry in a deployment unit's content structure. + * + * <p>A unit entry can be either a file or a folder within a deployment unit. + * This interface provides basic operations to access the entry's metadata such as name and size. + * + * @see UnitFile + * @see UnitFolder + */ +public interface UnitEntry { + /** + * Returns the name of this entry. + * + * <p>For files, this is the file name. For folders, this is the folder name. + * + * @return the entry name, never {@code null} + */ + String name(); + + /** + * Returns the size of this entry in bytes. + * + * <p>For files, this is the actual file size. For folders, this is the sum + * of all children sizes (recursive). + * + * @return the entry size in bytes, always non-negative + */ + long size(); +} diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitFile.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitFile.java new file mode 100644 index 00000000000..5db505599c7 --- /dev/null +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitFile.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.ignite.internal.deployunit.structure; + +import java.util.Objects; + +/** + * Represents a file entry in a deployment unit. + * + * <p>This implementation of {@link UnitEntry} holds metadata about a single file, + * including its name and size in bytes. + */ +public class UnitFile implements UnitEntry { + /** The name of the file. */ + private final String name; + + /** The size of the file in bytes. */ + private final long size; + + /** + * Creates a new file entry. + * + * @param name the file name + * @param size the file size in bytes + */ + public UnitFile(String name, long size) { + this.name = name; + this.size = size; + } + + @Override + public String name() { + return name; + } + + @Override + public long size() { + return size; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + UnitFile unitFile = (UnitFile) o; + return size == unitFile.size && Objects.equals(name, unitFile.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, size); + } +} diff --git a/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitFolder.java b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitFolder.java new file mode 100644 index 00000000000..5b81e08f9cd --- /dev/null +++ b/modules/code-deployment/src/main/java/org/apache/ignite/internal/deployunit/structure/UnitFolder.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.ignite.internal.deployunit.structure; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Represents a folder (directory) entry in a deployment unit. + * + * <p>This implementation of {@link UnitEntry} holds metadata about a folder, + * including its name and children entries. The folder can contain both files + * ({@link UnitFile}) and other folders ({@link UnitFolder}), creating a hierarchical structure. + * + * <p>The size of a folder is calculated as the sum of all its children's sizes. + */ +public class UnitFolder implements UnitEntry { + /** The name of the folder. */ + private final String name; + + /** The list of children entries in this folder. */ + private final List<UnitEntry> children = new ArrayList<>(); + + /** + * Creates a new folder entry with the specified name. + * + * @param name the folder name + */ + public UnitFolder(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public long size() { + return children.stream().mapToLong(UnitEntry::size).sum(); + } + + /** + * Adds a child entry to this folder. + * + * @param entry the child entry to add (file or folder) + */ + public void addChild(UnitEntry entry) { + children.add(entry); + } + + /** + * Returns an unmodifiable collection of children entries. + * + * @return the children entries, never {@code null} + */ + public Collection<UnitEntry> children() { + return Collections.unmodifiableCollection(children); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + UnitFolder folder = (UnitFolder) o; + return Objects.equals(name, folder.name) && Objects.equals(children, folder.children); + } + + @Override + public int hashCode() { + return Objects.hash(name, children); + } +} diff --git a/modules/code-deployment/src/test/java/org/apache/ignite/deployment/FileDeployerServiceTest.java b/modules/code-deployment/src/test/java/org/apache/ignite/deployment/FileDeployerServiceTest.java index d21ea58cca6..ce778b2346b 100644 --- a/modules/code-deployment/src/test/java/org/apache/ignite/deployment/FileDeployerServiceTest.java +++ b/modules/code-deployment/src/test/java/org/apache/ignite/deployment/FileDeployerServiceTest.java @@ -19,8 +19,11 @@ package org.apache.ignite.deployment; import static org.apache.ignite.deployment.version.Version.parseVersion; import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; @@ -38,6 +41,8 @@ import org.apache.ignite.internal.deployunit.FileDeployerService; import org.apache.ignite.internal.deployunit.StreamDeploymentUnit; import org.apache.ignite.internal.deployunit.UnitContent; import org.apache.ignite.internal.deployunit.exception.DeploymentUnitReadException; +import org.apache.ignite.internal.deployunit.structure.UnitFile; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; import org.apache.ignite.internal.testframework.IgniteTestUtils; import org.apache.ignite.internal.testframework.WorkDirectory; import org.apache.ignite.internal.testframework.WorkDirectoryExtension; @@ -86,6 +91,35 @@ public class FileDeployerServiceTest { } } + @Test + public void testUnitStructure() throws Exception { + try (StreamDeploymentUnit unit = content()) { + CompletableFuture<Boolean> deployed = service.deploy("id", parseVersion("1.0.0"), unit); + assertThat(deployed, willBe(true)); + } + + CompletableFuture<UnitFolder> folderFuture = service.getUnitStructure("id", parseVersion("1.0.0")); + assertThat(folderFuture, willCompleteSuccessfully()); + + UnitFolder folder = folderFuture.get(); + + assertThat(folder.children(), + containsInAnyOrder( + Stream.of(file1, file2, file3) + .map(FileDeployerServiceTest::toUnitFile).toArray() + ) + ); + } + + private static UnitFile toUnitFile(Path file) { + try { + return new UnitFile(file.getFileName().toString(), Files.size(file)); + } catch (IOException e) { + fail(e); + return null; + } + } + private StreamDeploymentUnit content() { Map<String, InputStream> map = Stream.of(file1, file2, file3) .collect(Collectors.toMap(it -> it.getFileName().toString(), it -> { diff --git a/modules/compute/src/test/java/org/apache/ignite/internal/compute/util/DummyIgniteDeployment.java b/modules/compute/src/test/java/org/apache/ignite/internal/compute/util/DummyIgniteDeployment.java index be75511e21a..eb43ae31bc9 100644 --- a/modules/compute/src/test/java/org/apache/ignite/internal/compute/util/DummyIgniteDeployment.java +++ b/modules/compute/src/test/java/org/apache/ignite/internal/compute/util/DummyIgniteDeployment.java @@ -35,6 +35,7 @@ import org.apache.ignite.internal.deployunit.IgniteDeployment; import org.apache.ignite.internal.deployunit.NodesToDeploy; import org.apache.ignite.internal.deployunit.UnitStatuses; import org.apache.ignite.internal.deployunit.exception.DeploymentUnitNotFoundException; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; import org.apache.ignite.internal.manager.ComponentContext; /** @@ -118,6 +119,11 @@ public class DummyIgniteDeployment implements IgniteDeployment { .orElseThrow()); } + @Override + public CompletableFuture<UnitFolder> nodeUnitFileStructure(String unitId, Version version) { + throw new UnsupportedOperationException("Not implemented"); + } + @Override public CompletableFuture<Void> startAsync(ComponentContext componentContext) { return nullCompletedFuture(); diff --git a/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java b/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java index 08bcbf50428..02157f553c0 100644 --- a/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java +++ b/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java @@ -428,7 +428,7 @@ public final class IgniteTestUtils { * @param t Throwable to check. * @param cls Cause classes to check. * @param msg Message text that should be in cause (if {@code null}, message won't be checked). - * @return reference to the cause error if found, otherwise returns {@code null}. + * @return reference to the cause error if found, otherwise returns {@code null}.27 */ public static <T extends Throwable> @Nullable T cause( Throwable t, diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java index 5a1d13cc293..fbfeeb511cc 100644 --- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/DeploymentCodeApi.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import org.apache.ignite.internal.rest.api.Problem; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry.UnitFolder; import org.reactivestreams.Publisher; /** @@ -234,4 +235,28 @@ public interface DeploymentCodeApi { @Schema(name = "statuses", description = "Deployment status filter.") Optional<List<DeploymentStatus>> statuses ); + + /** + * Unit content REST method. + */ + @Operation( + operationId = "unitContent", + summary = "Get unit contents.", + description = "Returns a folder representation with unit content." + ) + @ApiResponse(responseCode = "200", + description = "Unit content returned successfully.", + content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = UnitFolder.class)) + ) + @ApiResponse(responseCode = "500", + description = "Internal error.", + content = @Content(mediaType = PROBLEM_JSON, schema = @Schema(implementation = Problem.class)) + ) + @Get("node/units/structure/{unitId}/{unitVersion}") + CompletableFuture<UnitFolder> unitStructure( + @Schema(name = "unitId", description = "The ID of the deployment unit.") + String unitId, + @Schema(name = "unitVersion", description = "The version of the deployment unit.") + String unitVersion + ); } diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/UnitEntry.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/UnitEntry.java new file mode 100644 index 00000000000..0616ca2f810 --- /dev/null +++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/deployment/UnitEntry.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.ignite.internal.rest.api.deployment; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; +import java.util.List; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry.UnitFile; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry.UnitFolder; + +/** + * Represents an entry in a deployment unit's content structure for REST API responses. + * + * <p>A unit entry can be either a {@link UnitFile} or a {@link UnitFolder}. + * This interface is used for JSON serialization/deserialization in the REST API, + * with polymorphic type handling based on the "type" property. + * + * <p>The JSON representation includes a discriminator field "type" that can be either + * "file" or "folder" to distinguish between the two implementations. + */ +@Schema(description = "Unit content entry.") +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @Type(value = UnitFile.class, name = "file"), + @Type(value = UnitFolder.class, name = "folder") +}) +public interface UnitEntry { + /** + * Returns the name of this entry. + * + * @return the entry name + */ + @JsonGetter("name") + String name(); + + /** + * Returns the size of this entry in bytes. + * + * <p>For files, this is the actual file size. For folders, this is the sum + * of all children sizes (recursive). + * + * @return the entry size in bytes + */ + @JsonGetter("size") + long size(); + + /** + * Represents a file entry in a deployment unit for REST API responses. + * + * <p>This nested class implements {@link UnitEntry} and is serialized with + * a "type": "file" discriminator in JSON responses. + */ + @Schema(description = "Unit content file.") + public class UnitFile implements UnitEntry { + @Schema(description = "Unit content file name.", requiredMode = RequiredMode.REQUIRED) + private final String name; + + @Schema(description = "Unit content file size in bytes.", requiredMode = RequiredMode.REQUIRED) + private final long size; + + /** + * Creates a new file entry for JSON deserialization. + * + * @param name the file name + * @param size the file size in bytes + */ + @JsonCreator + public UnitFile( + @JsonProperty("name") String name, + @JsonProperty("size") long size + ) { + this.name = name; + this.size = size; + } + + /** + * Returns the name of this file. + * + * @return the file name + */ + @JsonGetter("name") + @Override + public String name() { + return name; + } + + /** + * Returns the size of this file in bytes. + * + * @return the file size in bytes + */ + @JsonGetter("size") + @Override + public long size() { + return size; + } + } + + /** + * Represents a folder (directory) entry in a deployment unit for REST API responses. + * + * <p>This nested class implements {@link UnitEntry} and is serialized with + * a "type": "folder" discriminator in JSON responses. It contains a list of + * children entries, which can be either files or other folders, creating a + * hierarchical structure. + * + * <p>The size of a folder is calculated as the sum of all its children's sizes. + */ + @Schema(description = "Unit content folder.") + public class UnitFolder implements UnitEntry { + @Schema(description = "Unit content folder name.", requiredMode = RequiredMode.REQUIRED) + private final String name; + + @Schema(description = "Unit content folder elements.", requiredMode = RequiredMode.REQUIRED) + private final List<UnitEntry> children; + + /** + * Creates a new folder entry for JSON deserialization. + * + * @param name the folder name + * @param children the list of children entries + */ + @JsonCreator + public UnitFolder( + @JsonProperty("name") String name, + @JsonProperty("children") List<UnitEntry> children + ) { + this.name = name; + this.children = children; + } + + /** + * Returns the name of this folder. + * + * @return the folder name + */ + @JsonGetter("name") + @Override + public String name() { + return name; + } + + /** + * Returns the total size of this folder in bytes. + * + * <p>The size is calculated as the sum of all children sizes. + * + * @return the total folder size in bytes + */ + @Override + public long size() { + return children.stream().mapToLong(UnitEntry::size).sum(); + } + + /** + * Returns the list of children entries in this folder. + * + * @return the list of children entries + */ + @JsonGetter("children") + public List<UnitEntry> children() { + return children; + } + } +} diff --git a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java index 16dc06fce3a..62639bc9681 100644 --- a/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java +++ b/modules/rest/src/integrationTest/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementControllerTest.java @@ -59,6 +59,9 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.ignite.internal.ClusterConfiguration; import org.apache.ignite.internal.ClusterPerClassIntegrationTest; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry.UnitFile; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry.UnitFolder; import org.apache.ignite.internal.rest.api.deployment.UnitStatus; import org.apache.ignite.internal.rest.api.deployment.UnitVersionStatus; import org.junit.jupiter.api.AfterEach; @@ -283,6 +286,45 @@ public class DeploymentManagementControllerTest extends ClusterPerClassIntegrati ); } + @Test + public void testUnitContent() { + String id = UNIT_ID; + String version = "1.1.1"; + + assertThat(deploy(id, version, false, smallFile, zipFile), hasStatus(OK)); + + awaitDeployedStatus(id, version); + + UnitFolder folder = folder(id, version); + + Path workDir0 = CLUSTER.nodeWorkDir(0); + Path nodeUnitDirectory = workDir0.resolve("deployment").resolve(id).resolve(version); + + for (UnitEntry child : folder.children()) { + verifyEntry(child, nodeUnitDirectory); + } + } + + private static void verifyEntry(UnitEntry entry, Path currentDir) { + try { + if (entry instanceof UnitFile) { + UnitFile file = (UnitFile) entry; + Path filePath = currentDir.resolve(file.name()); + assertTrue(Files.exists(filePath)); + assertThat(Files.size(filePath), is(file.size())); + } else if (entry instanceof UnitFolder) { + Path dir = currentDir.resolve(entry.name()); + for (UnitEntry child : ((UnitFolder) entry).children()) { + verifyEntry(child, dir); + } + } else { + fail(new IllegalStateException("Unit entry type not supported.")); + } + } catch (IOException e) { + fail(e); + } + } + private void awaitDeployedStatus(String id, String... versions) { await().untilAsserted(() -> { MutableHttpRequest<Object> get = HttpRequest.GET("cluster/units"); @@ -336,4 +378,10 @@ public class DeploymentManagementControllerTest extends ClusterPerClassIntegrati return client.toBlocking().retrieve(get, Argument.listOf(UnitStatus.class)); } + + private UnitFolder folder(String id, String version) { + MutableHttpRequest<Object> get = HttpRequest.GET("node/units/structure/" + id + "/" + version); + + return client.toBlocking().retrieve(get, UnitFolder.class); + } } diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java index e0d810c03aa..ace212efb69 100644 --- a/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java +++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/deployment/DeploymentManagementController.java @@ -36,6 +36,8 @@ import org.apache.ignite.deployment.version.Version; import org.apache.ignite.internal.deployunit.IgniteDeployment; import org.apache.ignite.internal.deployunit.NodesToDeploy; import org.apache.ignite.internal.deployunit.UnitStatuses; +import org.apache.ignite.internal.deployunit.structure.UnitFile; +import org.apache.ignite.internal.deployunit.structure.UnitFolder; import org.apache.ignite.internal.deployunit.tempstorage.TempStorage; import org.apache.ignite.internal.deployunit.tempstorage.TempStorageProvider; import org.apache.ignite.internal.logger.IgniteLogger; @@ -44,6 +46,7 @@ import org.apache.ignite.internal.rest.ResourceHolder; import org.apache.ignite.internal.rest.api.deployment.DeploymentCodeApi; import org.apache.ignite.internal.rest.api.deployment.DeploymentStatus; import org.apache.ignite.internal.rest.api.deployment.InitialDeployMode; +import org.apache.ignite.internal.rest.api.deployment.UnitEntry; import org.apache.ignite.internal.rest.api.deployment.UnitStatus; import org.apache.ignite.internal.rest.api.deployment.UnitVersionStatus; import org.jetbrains.annotations.Nullable; @@ -251,6 +254,28 @@ public class DeploymentManagementController implements DeploymentCodeApi, Resour return DeploymentStatus.valueOf(status.name()); } + @Override + public CompletableFuture<UnitEntry.UnitFolder> unitStructure(String unitId, String unitVersion) { + return deployment.nodeUnitFileStructure(unitId, parseVersion(unitVersion)).thenApply(DeploymentManagementController::toDto); + } + + private static UnitEntry.UnitFolder toDto(UnitFolder unitFolder) { + return new UnitEntry.UnitFolder( + unitFolder.name(), + unitFolder.children().stream() + .map(DeploymentManagementController::toDto) + .collect(Collectors.toList()) + ); + } + + private static UnitEntry toDto(org.apache.ignite.internal.deployunit.structure.UnitEntry entry) { + if (entry instanceof UnitFile) { + return new UnitEntry.UnitFile(entry.name(), entry.size()); + } else { + return toDto((UnitFolder) entry); + } + } + @Override public void cleanResources() { deployment = null;
