From c55adfc1acd31e33f6e1ec887520d61073651deb Mon Sep 17 00:00:00 2001 From: Tigran Manasyan Date: Thu, 18 Dec 2025 18:27:05 +0400 Subject: [PATCH] [ADH-7291] Add Docker test environment for Ozone --- pom.xml | 8 +- smart-ozone-support/pom.xml | 1 + .../smart-ozone-common/pom.xml | 61 +++++ .../org/smartdata/ozone/OzoneSmartConf.java | 0 .../smartdata/ozone/OzoneClusterCompose.java | 65 +++++ .../smartdata/ozone/OzoneClusterHarness.java | 81 ++++++ .../test/resources/docker-compose-ozone.yaml | 64 +++++ .../smart-ozone-fs-client/pom.xml | 24 +- .../ozone/client/FileAccessReportSupport.java | 6 +- .../fs/ozone/SmartOzoneClientAdapterTest.java | 64 +++-- .../src/test/resources/log4j2.properties | 19 ++ smart-ozone-support/smart-ozone/pom.xml | 24 +- .../ozone/snapshot/OfsSnapshotFetcher.java | 10 +- .../snapshot/OfsSnapshotFetcherTest.java | 239 ++++++++++++++++-- .../OfsSnapshotPathTransformerTest.java | 56 ++++ .../src/test/resources/log4j2.properties | 19 ++ smart-server/pom.xml | 7 + .../server/OzoneSmartClusterHarness.java | 25 ++ 18 files changed, 706 insertions(+), 67 deletions(-) create mode 100644 smart-ozone-support/smart-ozone-common/pom.xml rename smart-ozone-support/{smart-ozone => smart-ozone-common}/src/main/java/org/smartdata/ozone/OzoneSmartConf.java (100%) create mode 100644 smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterCompose.java create mode 100644 smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterHarness.java create mode 100644 smart-ozone-support/smart-ozone-common/src/test/resources/docker-compose-ozone.yaml create mode 100644 smart-ozone-support/smart-ozone-fs-client/src/test/resources/log4j2.properties create mode 100644 smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotPathTransformerTest.java create mode 100644 smart-ozone-support/smart-ozone/src/test/resources/log4j2.properties create mode 100644 smart-server/src/test/java/org/smartdata/server/OzoneSmartClusterHarness.java diff --git a/pom.xml b/pom.xml index aa06d2a88b..92d1f3fbb2 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 2.11.12 2.11 2.28.2 - 1.20.4 + 1.21.4 1.68 3.7.1 2.5.3 @@ -594,6 +594,12 @@ ${testcontainers.version} test + + org.testcontainers + testcontainers + ${testcontainers.version} + test + org.testcontainers postgresql diff --git a/smart-ozone-support/pom.xml b/smart-ozone-support/pom.xml index f66cff76dd..217a49773d 100644 --- a/smart-ozone-support/pom.xml +++ b/smart-ozone-support/pom.xml @@ -35,6 +35,7 @@ smart-ozone smart-ozone-fs-client + smart-ozone-common \ No newline at end of file diff --git a/smart-ozone-support/smart-ozone-common/pom.xml b/smart-ozone-support/smart-ozone-common/pom.xml new file mode 100644 index 0000000000..cfdbaba89d --- /dev/null +++ b/smart-ozone-support/smart-ozone-common/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + + org.smartdata + smart-ozone-support + 2.2.0-SNAPSHOT + + + smart-ozone-common + 2.2.0-SNAPSHOT + jar + + + + org.smartdata + smart-hadoop-common + 2.2.0-SNAPSHOT + + + + org.smartdata + smart-common + 2.2.0-SNAPSHOT + + + + org.apache.ozone + ozone-filesystem-hadoop3 + ${ozone.version} + + + + org.testcontainers + testcontainers + test + + + \ No newline at end of file diff --git a/smart-ozone-support/smart-ozone/src/main/java/org/smartdata/ozone/OzoneSmartConf.java b/smart-ozone-support/smart-ozone-common/src/main/java/org/smartdata/ozone/OzoneSmartConf.java similarity index 100% rename from smart-ozone-support/smart-ozone/src/main/java/org/smartdata/ozone/OzoneSmartConf.java rename to smart-ozone-support/smart-ozone-common/src/main/java/org/smartdata/ozone/OzoneSmartConf.java diff --git a/smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterCompose.java b/smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterCompose.java new file mode 100644 index 0000000000..eece5c7227 --- /dev/null +++ b/smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterCompose.java @@ -0,0 +1,65 @@ +/** + * 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.smartdata.ozone; + +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.io.File; + +import static org.smartdata.ozone.OzoneClusterHarness.resourceAbsolutePath; + +public class OzoneClusterCompose extends ComposeContainer { + public static final String DEFAULT_COMPOSE_FILE = "docker-compose-ozone.yaml"; + + private static final String OM_SERVICE = "om"; + private static final String SCM_SERVICE = "scm"; + private static final String DATANODE_SERVICE = "datanode"; + + private static final int OM_PORT = 9862; + private static final int SCM_PORT = 9876; + private static final int DATANODE_PORT = 9864; + + public OzoneClusterCompose() { + this(resourceAbsolutePath(DEFAULT_COMPOSE_FILE)); + } + + public OzoneClusterCompose(String composeFilePath) { + super(new File(composeFilePath)); + + withExposedService(DATANODE_SERVICE, DATANODE_PORT, + Wait.forLogMessage(".*Ozone container server started.*", 1)); + withExposedService(SCM_SERVICE, SCM_PORT, + Wait.forLogMessage(".*SCM exiting safe mode.*", 1)); + withExposedService(OM_SERVICE, OM_PORT, + Wait.forLogMessage(".*Leader om1@.* is ready.*", 1)); + } + + public String getOmHost() { + return getServiceHost(OM_SERVICE, OM_PORT); + } + + public int getOmPort() { + return getServicePort(OM_SERVICE, OM_PORT); + } + + public String getOmRpcAddress() { + return getOmHost() + ":" + getOmPort(); + } + +} diff --git a/smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterHarness.java b/smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterHarness.java new file mode 100644 index 0000000000..6f108cac09 --- /dev/null +++ b/smart-ozone-support/smart-ozone-common/src/test/java/org/smartdata/ozone/OzoneClusterHarness.java @@ -0,0 +1,81 @@ +/** + * 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.smartdata.ozone; + +import org.apache.hadoop.hdds.client.ReplicationConfig; +import org.apache.hadoop.hdds.client.ReplicationFactor; +import org.apache.hadoop.hdds.client.ReplicationType; +import org.apache.hadoop.ozone.client.ObjectStore; +import org.apache.hadoop.ozone.client.OzoneBucket; +import org.apache.hadoop.ozone.client.OzoneClientFactory; +import org.apache.hadoop.ozone.client.io.OzoneOutputStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.smartdata.conf.SmartConf; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Optional; + +import static org.smartdata.ozone.OzoneSmartConf.DEFAULT_OFS_ADDRESS; + +public class OzoneClusterHarness { + + private static final ReplicationConfig REPLICATION_CONFIG = + ReplicationConfig.fromTypeAndFactor(ReplicationType.RATIS, ReplicationFactor.ONE); + + @Rule + public OzoneClusterCompose ozoneContainer = new OzoneClusterCompose(); + + protected OzoneSmartConf ozoneConf; + protected ObjectStore ozoneClient; + + @Before + public void init() throws Exception { + SmartConf smartConf = new SmartConf(); + smartConf.set(DEFAULT_OFS_ADDRESS, "ofs://" + ozoneContainer.getOmRpcAddress()); + smartConf.set("ozone.om.address", ozoneContainer.getOmRpcAddress()); + + ozoneConf = new OzoneSmartConf(smartConf); + ozoneClient = OzoneClientFactory.getRpcClient(ozoneConf).getObjectStore(); + } + + @After + public void tearDown() throws IOException { + ozoneClient.getClientProxy().close(); + } + + public static void createKey(OzoneBucket bucket, String key, String data) throws IOException { + byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); + try (OzoneOutputStream outputStream = + bucket.createKey(key, dataBytes.length, REPLICATION_CONFIG, new HashMap<>())) { + outputStream.write(dataBytes); + outputStream.flush(); + } + } + + public static String resourceAbsolutePath(String relativePath) { + return Optional.ofNullable( + OzoneClusterHarness.class.getClassLoader().getResource(relativePath)) + .map(URL::getPath) + .orElseThrow(() -> new RuntimeException("Resource not found")); + } +} diff --git a/smart-ozone-support/smart-ozone-common/src/test/resources/docker-compose-ozone.yaml b/smart-ozone-support/smart-ozone-common/src/test/resources/docker-compose-ozone.yaml new file mode 100644 index 0000000000..29e2a8e35c --- /dev/null +++ b/smart-ozone-support/smart-ozone-common/src/test/resources/docker-compose-ozone.yaml @@ -0,0 +1,64 @@ +# 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. + +x-image: + &image + image: ${OZONE_IMAGE:-apache/ozone}:${OZONE_IMAGE_VERSION:-2.0.0}${OZONE_IMAGE_FLAVOR:-} + +x-common-config: + &common-config + OZONE-SITE.XML_hdds.datanode.dir: "/data/hdds" + OZONE-SITE.XML_ozone.metadata.dirs: "/data/metadata" + OZONE-SITE.XML_ozone.om.address: "om" + OZONE-SITE.XML_ozone.administrators: "hadoop,om,hdfs,${ADMIN_USER:-root}" + OZONE-SITE.XML_ozone.om.http-address: "om:9874" + OZONE-SITE.XML_ozone.replication: "1" + OZONE-SITE.XML_ozone.scm.block.client.address: "scm" + OZONE-SITE.XML_ozone.scm.client.address: "scm" + OZONE-SITE.XML_ozone.scm.datanode.id.dir: "/data/metadata" + OZONE-SITE.XML_ozone.scm.names: "scm" + no_proxy: "om,scm,localhost,127.0.0.1" + +version: "3" +services: + datanode: + <<: *image + ports: + - 9864 + command: [ "ozone","datanode" ] + environment: + <<: *common-config + + om: + <<: *image + ports: + - 9862 + environment: + <<: *common-config + CORE-SITE.XML_hadoop.proxyuser.hadoop.hosts: "*" + CORE-SITE.XML_hadoop.proxyuser.hadoop.groups: "*" + ENSURE_OM_INITIALIZED: /data/metadata/om/current/VERSION + WAITFOR: scm:9876 + command: [ "ozone","om" ] + + scm: + <<: *image + ports: + - 9876 + environment: + <<: *common-config + ENSURE_SCM_INITIALIZED: /data/metadata/scm/current/VERSION + command: [ "ozone","scm" ] diff --git a/smart-ozone-support/smart-ozone-fs-client/pom.xml b/smart-ozone-support/smart-ozone-fs-client/pom.xml index 136e8ef103..8ba6a076cf 100644 --- a/smart-ozone-support/smart-ozone-fs-client/pom.xml +++ b/smart-ozone-support/smart-ozone-fs-client/pom.xml @@ -35,7 +35,7 @@ org.smartdata - smart-hadoop-common + smart-ozone-common 2.2.0-SNAPSHOT @@ -43,16 +43,6 @@ smart-client 2.2.0-SNAPSHOT - - org.smartdata - smart-common - 2.2.0-SNAPSHOT - - - org.apache.ozone - ozone-filesystem-hadoop3 - ${ozone.version} - org.projectlombok lombok @@ -78,6 +68,18 @@ mockito-core test + + org.smartdata + smart-ozone-common + 2.2.0-SNAPSHOT + test + test-jar + + + org.testcontainers + testcontainers + test + diff --git a/smart-ozone-support/smart-ozone-fs-client/src/main/java/org/smartdata/ozone/client/FileAccessReportSupport.java b/smart-ozone-support/smart-ozone-fs-client/src/main/java/org/smartdata/ozone/client/FileAccessReportSupport.java index 37726f4fef..45112b46a3 100644 --- a/smart-ozone-support/smart-ozone-fs-client/src/main/java/org/smartdata/ozone/client/FileAccessReportSupport.java +++ b/smart-ozone-support/smart-ozone-fs-client/src/main/java/org/smartdata/ozone/client/FileAccessReportSupport.java @@ -44,7 +44,7 @@ public FileAccessReportSupport(SmartClientProtocol ssmClient, String... basePath } public void reportFileAccess(String path) { - String pathWithoutAuthority = new Path(basePath, path) + String pathWithoutAuthority = new Path(basePath, removeLeadingSlash(path)) .toUri() .getPath(); @@ -60,6 +60,10 @@ public void reportFileAccess(String path) { } } + private String removeLeadingSlash(String path) { + return path.startsWith("/") ? path.substring(1) : path; + } + @Override public void close() throws IOException { ssmClient.close(); diff --git a/smart-ozone-support/smart-ozone-fs-client/src/test/java/org/apache/hadoop/fs/ozone/SmartOzoneClientAdapterTest.java b/smart-ozone-support/smart-ozone-fs-client/src/test/java/org/apache/hadoop/fs/ozone/SmartOzoneClientAdapterTest.java index 11824a701f..a67732f920 100644 --- a/smart-ozone-support/smart-ozone-fs-client/src/test/java/org/apache/hadoop/fs/ozone/SmartOzoneClientAdapterTest.java +++ b/smart-ozone-support/smart-ozone-fs-client/src/test/java/org/apache/hadoop/fs/ozone/SmartOzoneClientAdapterTest.java @@ -18,17 +18,20 @@ package org.apache.hadoop.fs.ozone; import org.apache.hadoop.fs.Path; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.smartdata.metrics.FileAccessEvent; import org.smartdata.model.FileState; +import org.smartdata.ozone.OzoneClusterHarness; import org.smartdata.ozone.client.SmartOzoneClientAdapter; import org.smartdata.protocol.SmartClientProtocol; import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,56 +39,81 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -public class SmartOzoneClientAdapterTest { +public class SmartOzoneClientAdapterTest extends OzoneClusterHarness { + private static final String TEST_VOLUME = "vol1"; + private static final String TEST_BUCKET = "buck1"; + + private static final String TEST_DATA = "data_777"; private MockSsmClient ssmClient; @Before - public void init() { + public void initClient() throws IOException { this.ssmClient = new MockSsmClient(); + + ozoneClient.createVolume(TEST_VOLUME); + ozoneClient.getVolume(TEST_VOLUME) + .createBucket(TEST_BUCKET); } @Test - @Ignore("Unignore when testing environment for Ozone will be added (ADH-7291)") - public void testReportAccessEventOfs() throws IOException { + public void testReportAccessEventOfs() throws Exception { SmartOzoneClientAdapter clientAdapter = new SmartRootedOzoneFileSystem.SmartClientAdapter( - "TODO", -1, new OzoneConfiguration(), null, ssmClient); - testReportAccessEventInternal(clientAdapter); + ozoneContainer.getOmHost(), ozoneContainer.getOmPort(), + ozoneConf, null, ssmClient); + testReportAccessEventInternal(clientAdapter, TEST_VOLUME, TEST_BUCKET); } @Test - @Ignore("Unignore when testing environment for Ozone will be added (ADH-7291)") - public void testReportAccessEventO3fs() throws IOException { + public void testReportAccessEventO3fs() throws Exception { SmartOzoneClientAdapter clientAdapter = new SmartOzoneFileSystem.SmartClientAdapter( - "TODO", -1, new OzoneConfiguration(), "someVolume", "someBucket", null, ssmClient); + ozoneContainer.getOmHost(), ozoneContainer.getOmPort(), + ozoneConf, TEST_VOLUME, TEST_BUCKET, null, ssmClient); testReportAccessEventInternal(clientAdapter); } - public void testReportAccessEventInternal(SmartOzoneClientAdapter clientAdapter) throws IOException { - clientAdapter.createFile("key", (short) 1, false, false); + private void testReportAccessEventInternal(SmartOzoneClientAdapter clientAdapter, String... prefixSegments) + throws Exception { + String keyPrefix = Arrays.stream(prefixSegments) + .collect(Collectors.joining("/", "", "/")); + + createFile(clientAdapter, keyPrefix + "key"); + createFile(clientAdapter, keyPrefix + "anotherKey"); + createFile(clientAdapter, keyPrefix + "keyToRemove"); assertTrue(ssmClient.accessEvents.isEmpty()); - clientAdapter.getFileStatus("key1", null, null, null); + clientAdapter.getFileStatus(keyPrefix + "anotherKey", + new URI("ofs://test:7070"), new Path(keyPrefix, "anotherKey"), "anon"); assertTrue(ssmClient.accessEvents.isEmpty()); - clientAdapter.readFile("someKey"); + clientAdapter.deleteObject(keyPrefix + "keyToRemove"); + assertTrue(ssmClient.accessEvents.isEmpty()); + + clientAdapter.readFile(keyPrefix + "key").close(); assertEquals(1, ssmClient.accessEvents.size()); - clientAdapter.readFile("someDir/anotherKey"); + clientAdapter.readFile(keyPrefix + "anotherKey").close(); assertEquals(2, ssmClient.accessEvents.size()); List actualAccessedFiles = ssmClient.accessEvents.stream() .map(FileAccessEvent::getPath) .collect(Collectors.toList()); - List expectedAccessFiles = Stream.of("someKey", "someDir/anotherKey") - .map(path -> new Path(clientAdapter.getBasePath(), path)) + List expectedAccessFiles = Stream.of("key", "anotherKey") + .map(path -> new Path(new Path("/" + TEST_VOLUME, TEST_BUCKET), path)) .map(path -> path.toUri().getPath()) .collect(Collectors.toList()); assertEquals(expectedAccessFiles, actualAccessedFiles); } + private void createFile(SmartOzoneClientAdapter clientAdapter, String key) throws IOException { + try (OzoneFSOutputStream outputStream = clientAdapter.createFile(key, (short) 1, true, true)) { + outputStream.write(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + outputStream.flush(); + } + } + private static class MockSsmClient implements SmartClientProtocol { private final List accessEvents = new ArrayList<>(); diff --git a/smart-ozone-support/smart-ozone-fs-client/src/test/resources/log4j2.properties b/smart-ozone-support/smart-ozone-fs-client/src/test/resources/log4j2.properties new file mode 100644 index 0000000000..9eaa484e2a --- /dev/null +++ b/smart-ozone-support/smart-ozone-fs-client/src/test/resources/log4j2.properties @@ -0,0 +1,19 @@ +# 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. +# log4j configuration used during build and unit tests + +rootLogger = INFO, STDOUT + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{ISO8601} %-5p %c{2} (%F:%M(%L)) - %m%n diff --git a/smart-ozone-support/smart-ozone/pom.xml b/smart-ozone-support/smart-ozone/pom.xml index add152833b..b1e6e86388 100644 --- a/smart-ozone-support/smart-ozone/pom.xml +++ b/smart-ozone-support/smart-ozone/pom.xml @@ -36,19 +36,9 @@ org.smartdata - smart-hadoop-common + smart-ozone-common 2.2.0-SNAPSHOT - - org.smartdata - smart-common - 2.2.0-SNAPSHOT - - - org.apache.ozone - ozone-filesystem-hadoop3 - ${ozone.version} - org.projectlombok lombok @@ -84,5 +74,17 @@ mockito-core test + + org.smartdata + smart-ozone-common + 2.2.0-SNAPSHOT + test + test-jar + + + org.testcontainers + testcontainers + test + \ No newline at end of file diff --git a/smart-ozone-support/smart-ozone/src/main/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcher.java b/smart-ozone-support/smart-ozone/src/main/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcher.java index 341b68a2fe..42f12930df 100644 --- a/smart-ozone-support/smart-ozone/src/main/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcher.java +++ b/smart-ozone-support/smart-ozone/src/main/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcher.java @@ -148,7 +148,7 @@ private CompletableFuture handleVolume(OzoneVolume volume) { private CompletableFuture handleVolume(FileStatus fileStatus) { OzoneFileInfo.Builder fileBuilder = OzoneFileInfo.builder() - .path(fileStatus.getPath().toString()) + .path(pathWithoutAuthority(fileStatus.getPath())) .isVolume(true); return saveFile(fileStatus, fileBuilder) .thenComposeAsync(ignore -> executeInParallel( @@ -158,7 +158,7 @@ private CompletableFuture handleVolume(FileStatus fileStatus) { private CompletableFuture handleBucket(FileStatus fileStatus) { OzoneFileInfo.Builder fileBuilder = OzoneFileInfo.builder() - .path(fileStatus.getPath().toString()) + .path(pathWithoutAuthority(fileStatus.getPath())) .isBucket(true); return saveFile(fileStatus, fileBuilder) .thenComposeAsync(ignore -> createBucketSnapshot(fileStatus), executor) @@ -172,7 +172,7 @@ private CompletableFuture handleBucket(FileStatus fileStatus) { private CompletableFuture handleKey(FileStatus fileStatus) { Path filePath = getOriginalFilePath(fileStatus.getPath()); OzoneFileInfo.Builder fileBuilder = OzoneFileInfo.builder() - .path(filePath.toString()); + .path(pathWithoutAuthority(filePath)); return saveFile(fileStatus, fileBuilder) .thenComposeAsync(ignore -> handleChildrenIfDirectory(fileStatus), executor); } @@ -360,6 +360,10 @@ Path getOriginalFilePath(Path snapshotPath) { ); } + private String pathWithoutAuthority(Path path) { + return path.toUri().getPath(); + } + private String ofsSnapshotKeyToPath(OFSPath ofsPath) { String ofsKey = ofsPath.getKeyName(); int snapshotsDirEnd = ofsKey.indexOf(PATH_DELIMITER); diff --git a/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcherTest.java b/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcherTest.java index 487f50b0e6..9eeb6197cf 100644 --- a/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcherTest.java +++ b/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotFetcherTest.java @@ -17,40 +17,235 @@ */ package org.smartdata.ozone.snapshot; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import lombok.Data; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.ozone.client.OzoneBucket; +import org.apache.hadoop.ozone.client.OzoneVolume; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import org.smartdata.ozone.OzoneClusterHarness; +import org.smartdata.ozone.model.FsObjectStreamRecord; +import org.smartdata.ozone.model.OzoneFileInfo; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static java.util.stream.IntStream.range; import static org.junit.Assert.assertEquals; -public class OfsSnapshotFetcherTest { +public class OfsSnapshotFetcherTest extends OzoneClusterHarness { + private static final int SNAPSHOT_FETCH_TIMEOUT_SEC = 10; + + private static final int VOLUMES_COUNT = 2; + private static final int BUCKETS_PER_VOL_COUNT = 4; + private static final int KEYS_PER_BUCKET_COUNT = 3; + private static final int DIRS_PER_BUCKET_COUNT = 4; + private static final int SUBDIRS_PER_DIR_COUNT = 3; + private static final int KEYS_PER_DIR_COUNT = 3; + private static final int KEYS_PER_SUBDIR_COUNT = 2; + + private final static int TOTAL_EVENTS_PER_DIR_COUNT = + 1 + KEYS_PER_DIR_COUNT + SUBDIRS_PER_DIR_COUNT * (1 + KEYS_PER_SUBDIR_COUNT); + + private final static int TOTAL_EVENTS_PER_BUCKET_COUNT = + 1 + KEYS_PER_BUCKET_COUNT + DIRS_PER_BUCKET_COUNT * TOTAL_EVENTS_PER_DIR_COUNT; + + private final static int TOTAL_EVENTS_PER_VOLUME_COUNT = + 1 + BUCKETS_PER_VOL_COUNT * TOTAL_EVENTS_PER_BUCKET_COUNT; + + // 1 for EOF event and 1 for s3v bucket for s3 related keys + private final static int TOTAL_EVENTS_COUNT = + 2 + VOLUMES_COUNT * TOTAL_EVENTS_PER_VOLUME_COUNT; + + private OfsSnapshotFetcher fetcher; + + @Before + public void initFetcher() throws IOException { + fetcher = OfsSnapshotFetcher.builder() + .fs(FileSystem.get(ozoneConf)) + .objectStore(ozoneClient) + .conf(ozoneConf) + .executor(Executors.newFixedThreadPool(8)) + .batchSize(TOTAL_EVENTS_COUNT + 2) + .build(); + + initFs(); + } + + @After + public void closeFetcher() { + fetcher.close(); + } @Test - public void testExtractOriginalFilePath() { - testExtractOriginalFilePath("/vol/buck/.snapshot/sn1/key", "/vol/buck/key"); - testExtractOriginalFilePath("/vol/buck/.snapshot/sn1/dir/key", "/vol/buck/dir/key"); - testExtractOriginalFilePath("/vol/buck/.snapshot/sn1/dir/subdir/key", - "/vol/buck/dir/subdir/key"); + public void testFetchKeys() throws Exception { + fetcher.runSnapshotAsync().get(SNAPSHOT_FETCH_TIMEOUT_SEC, TimeUnit.SECONDS); + + BlockingQueue outputQueue = fetcher.getOutputQueue(); + assertEquals(TOTAL_EVENTS_COUNT, outputQueue.size()); + + Set actualFiles = outputQueue.stream() + .filter(OzoneFileInfo.class::isInstance) + .map(OzoneFileInfo.class::cast) + .map(this::toFileView) + .collect(Collectors.toSet()); + + assertEquals(expectedFiles(), actualFiles); + } + + private FileView toFileView(OzoneFileInfo fileInfo) { + return new FileView( + fileInfo.getPath(), + fileInfo.getLength(), + fileInfo.isVolume(), + fileInfo.isBucket() + ); + } + + private void initFs() { + range(0, VOLUMES_COUNT) + .forEach(this::initVolume); + } + + private Set expectedFiles() { + Set expected = new HashSet<>(); + + expected.add(volumeInfo("/s3v")); + + range(0, VOLUMES_COUNT).forEach(vIdx -> { + String volPath = "/vol" + vIdx; + expected.add(volumeInfo(volPath)); + + range(0, BUCKETS_PER_VOL_COUNT).forEach(bIdx -> { + String buckPath = volPath + "/buck" + bIdx; + expected.add(bucketInfo(buckPath)); + + range(0, KEYS_PER_BUCKET_COUNT).forEach(kIdx -> + expected.add(keyInfo(buckPath + "/key" + kIdx))); + + range(0, DIRS_PER_BUCKET_COUNT).forEach(dIdx -> { + String dirPath = buckPath + "/dir" + dIdx; + expected.add(dirInfo(dirPath)); + + range(0, KEYS_PER_DIR_COUNT).forEach(kIdx -> + expected.add(keyInfo(dirPath + "/key" + kIdx))); + + range(0, SUBDIRS_PER_DIR_COUNT).forEach(sdIdx -> { + String sdPath = dirPath + "/subdir" + sdIdx; + expected.add(dirInfo(sdPath)); + + range(0, KEYS_PER_SUBDIR_COUNT).forEach(kIdx -> + expected.add(keyInfo(sdPath + "/key" + kIdx))); + }); + }); + }); + }); - testExtractOriginalFilePath("ofs://host:123/vol/buck/.snapshot/sn1/dir/key", - "ofs://host:123/vol/buck/dir/key"); + return expected; + } + + private FileView volumeInfo(String path) { + return new FileView(path, 0, true, false); + } + + private FileView bucketInfo(String path) { + return new FileView(path, 0, false, true); + } + + private FileView dirInfo(String path) { + return new FileView(path, 0, false, false); + } + + private FileView keyInfo(String path) { + String fileData = Arrays.stream(path.split("/")) + // skip root, volume and bucket + .skip(3) + .collect(Collectors.joining("/")) + "_data"; - testExtractOriginalFilePath("/", "/"); - testExtractOriginalFilePath("/vol", "/vol"); - testExtractOriginalFilePath("/vol/buck", "/vol/buck"); + return new FileView(path, + fileData.getBytes(StandardCharsets.UTF_8).length, false, false); } - private void testExtractOriginalFilePath(String sourcePath, String expectedPath) { - try (OfsSnapshotFetcher fetcher = emptyFetcher()) { - Path actualPath = fetcher.getOriginalFilePath(new Path(sourcePath)); - assertEquals(expectedPath, actualPath.toString()); + private void initVolume(int volumeIdx) { + try { + String volumeName = "vol" + volumeIdx; + ozoneClient.createVolume(volumeName); + OzoneVolume volume = ozoneClient.getVolume(volumeName); + + range(0, BUCKETS_PER_VOL_COUNT) + .forEach(idx -> initBucket(volume, idx)); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize volume " + volumeIdx, e); } } - private OfsSnapshotFetcher emptyFetcher() { - return OfsSnapshotFetcher.builder() - .conf(new OzoneConfiguration()) - .batchSize(1) - .build(); + private void initBucket(OzoneVolume volume, int bucketIdx) { + try { + String bucketName = "buck" + bucketIdx; + volume.createBucket(bucketName); + OzoneBucket bucket = volume.getBucket(bucketName); + + range(0, KEYS_PER_BUCKET_COUNT).forEach(idx -> + createKey(bucket, "key" + idx)); + + range(0, DIRS_PER_BUCKET_COUNT).forEach(idx -> + initDirectory(bucket, idx)); + + } catch (IOException e) { + throw new RuntimeException("Failed to initialize bucket " + bucketIdx, e); + } + } + + private void initDirectory(OzoneBucket bucket, int dirIdx) { + try { + String dirPath = "dir" + dirIdx; + bucket.createDirectory(dirPath); + + range(0, KEYS_PER_DIR_COUNT).forEach(idx -> + createKey(bucket, dirPath + "/key" + idx)); + + range(0, SUBDIRS_PER_DIR_COUNT).forEach(idx -> + initSubdirectory(bucket, dirPath, idx)); + + } catch (IOException e) { + throw new RuntimeException("Failed to initialize directory " + dirIdx, e); + } + } + + private void initSubdirectory(OzoneBucket bucket, String parentPath, int dirIdx) { + try { + String subDirPath = parentPath + "/subdir" + dirIdx; + bucket.createDirectory(subDirPath); + + range(0, KEYS_PER_SUBDIR_COUNT).forEach(idx -> + createKey(bucket, subDirPath + "/key" + idx)); + + } catch (IOException e) { + throw new RuntimeException("Failed to initialize subdirectory " + dirIdx, e); + } + } + + private void createKey(OzoneBucket bucket, String keyName) { + try { + createKey(bucket, keyName, keyName + "_data"); + } catch (IOException e) { + throw new RuntimeException("Failed to create key: " + keyName, e); + } + } + + @Data + private static class FileView { + private final String path; + private final long length; + private final boolean isVolume; + private final boolean isBucket; } } \ No newline at end of file diff --git a/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotPathTransformerTest.java b/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotPathTransformerTest.java new file mode 100644 index 0000000000..c7c63360da --- /dev/null +++ b/smart-ozone-support/smart-ozone/src/test/java/org/smartdata/ozone/snapshot/OfsSnapshotPathTransformerTest.java @@ -0,0 +1,56 @@ +/** + * 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.smartdata.ozone.snapshot; + +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class OfsSnapshotPathTransformerTest { + + @Test + public void testExtractOriginalFilePath() { + testExtractOriginalFilePath("/vol/buck/.snapshot/sn1/key", "/vol/buck/key"); + testExtractOriginalFilePath("/vol/buck/.snapshot/sn1/dir/key", "/vol/buck/dir/key"); + testExtractOriginalFilePath("/vol/buck/.snapshot/sn1/dir/subdir/key", + "/vol/buck/dir/subdir/key"); + + testExtractOriginalFilePath("ofs://host:123/vol/buck/.snapshot/sn1/dir/key", + "ofs://host:123/vol/buck/dir/key"); + + testExtractOriginalFilePath("/", "/"); + testExtractOriginalFilePath("/vol", "/vol"); + testExtractOriginalFilePath("/vol/buck", "/vol/buck"); + } + + private void testExtractOriginalFilePath(String sourcePath, String expectedPath) { + try (OfsSnapshotFetcher fetcher = emptyFetcher()) { + Path actualPath = fetcher.getOriginalFilePath(new Path(sourcePath)); + assertEquals(expectedPath, actualPath.toString()); + } + } + + private OfsSnapshotFetcher emptyFetcher() { + return OfsSnapshotFetcher.builder() + .conf(new OzoneConfiguration()) + .batchSize(1) + .build(); + } +} \ No newline at end of file diff --git a/smart-ozone-support/smart-ozone/src/test/resources/log4j2.properties b/smart-ozone-support/smart-ozone/src/test/resources/log4j2.properties new file mode 100644 index 0000000000..9eaa484e2a --- /dev/null +++ b/smart-ozone-support/smart-ozone/src/test/resources/log4j2.properties @@ -0,0 +1,19 @@ +# 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. +# log4j configuration used during build and unit tests + +rootLogger = INFO, STDOUT + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{ISO8601} %-5p %c{2} (%F:%M(%L)) - %m%n diff --git a/smart-server/pom.xml b/smart-server/pom.xml index 1eff0e8404..c1f26dcde9 100644 --- a/smart-server/pom.xml +++ b/smart-server/pom.xml @@ -198,6 +198,13 @@ test test-jar + + org.smartdata + smart-ozone-common + 2.2.0-SNAPSHOT + test + test-jar + org.testcontainers jdbc diff --git a/smart-server/src/test/java/org/smartdata/server/OzoneSmartClusterHarness.java b/smart-server/src/test/java/org/smartdata/server/OzoneSmartClusterHarness.java new file mode 100644 index 0000000000..dc381c29d9 --- /dev/null +++ b/smart-server/src/test/java/org/smartdata/server/OzoneSmartClusterHarness.java @@ -0,0 +1,25 @@ +/** + * 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.smartdata.server; + +import org.smartdata.ozone.OzoneClusterHarness; + +// TODO init SmartServer when ADH-7056 will be completed +public class OzoneSmartClusterHarness extends OzoneClusterHarness { + protected SmartServer ssm; +}