blob: fc1abc19dd8d854517b001682d3a17620c76f2bf [file] [log] [blame]
/*
* 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.hadoop.fs.s3a.tools;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.contract.ContractTestUtils;
import org.apache.hadoop.fs.s3a.S3AFileSystem;
import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_AUTHORITATIVE;
import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_DELETE;
import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_KEEP;
import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.BucketInfo.BUCKET_INFO;
import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.runS3GuardCommand;
import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.runS3GuardCommandToFailure;
import static org.apache.hadoop.fs.s3a.tools.MarkerTool.*;
import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_INTERRUPTED;
import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_NOT_ACCEPTABLE;
import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_NOT_FOUND;
import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_USAGE;
/**
* Test the marker tool and use it to compare the behavior
* of keeping vs legacy S3A FS instances.
*/
public class ITestMarkerTool extends AbstractMarkerToolTest {
protected static final Logger LOG =
LoggerFactory.getLogger(ITestMarkerTool.class);
/**
* How many files to expect.
*/
private int expectedFileCount;
/**
* How many markers to expect under dir1.
*/
private int expectedMarkersUnderDir1;
/**
* How many markers to expect under dir2.
*/
private int expectedMarkersUnderDir2;
/**
* How many markers to expect across both dirs?
*/
private int expectedMarkers;
/**
* How many markers to expect including the base directory?
*/
private int expectedMarkersWithBaseDir;
@Test
public void testCleanMarkersLegacyDir() throws Throwable {
describe("Clean markers under a deleting FS -expect none");
CreatedPaths createdPaths = createPaths(getDeletingFS(), methodPath());
markerTool(getDeletingFS(), createdPaths.base, false, 0);
markerTool(getDeletingFS(), createdPaths.base, true, 0);
}
@Test
public void testCleanMarkersFileLimit() throws Throwable {
describe("Clean markers under a keeping FS -with file limit");
CreatedPaths createdPaths = createPaths(getKeepingFS(), methodPath());
// audit will be interrupted
markerTool(EXIT_INTERRUPTED, getDeletingFS(),
createdPaths.base, false, 0, 1, false);
}
@Test
public void testCleanMarkersKeepingDir() throws Throwable {
describe("Audit then clean markers under a deleting FS "
+ "-expect markers to be found and then cleaned up");
CreatedPaths createdPaths = createPaths(getKeepingFS(), methodPath());
// audit will find the expected entries
int expectedMarkerCount = createdPaths.dirs.size();
S3AFileSystem fs = getDeletingFS();
LOG.info("Auditing a directory with retained markers -expect failure");
markerTool(EXIT_NOT_ACCEPTABLE, fs,
createdPaths.base, false, 0, UNLIMITED_LISTING, false);
LOG.info("Auditing a directory expecting retained markers");
markerTool(fs, createdPaths.base, false,
expectedMarkerCount);
// we require that a purge didn't take place, so run the
// audit again.
LOG.info("Auditing a directory expecting retained markers");
markerTool(fs, createdPaths.base, false,
expectedMarkerCount);
LOG.info("Purging a directory of retained markers");
// purge cleans up
assertMarkersDeleted(expectedMarkerCount,
markerTool(fs, createdPaths.base, true, expectedMarkerCount));
// and a rerun doesn't find markers
LOG.info("Auditing a directory with retained markers -expect success");
assertMarkersDeleted(0,
markerTool(fs, createdPaths.base, true, 0));
}
@Test
public void testRenameKeepingFS() throws Throwable {
describe("Rename with the keeping FS -verify that no markers"
+ " exist at far end");
Path base = methodPath();
Path source = new Path(base, "source");
Path dest = new Path(base, "dest");
S3AFileSystem fs = getKeepingFS();
CreatedPaths createdPaths = createPaths(fs, source);
// audit will find three entries
int expectedMarkerCount = createdPaths.dirs.size();
markerTool(fs, source, false, expectedMarkerCount);
fs.rename(source, dest);
assertIsDirectory(dest);
// there are no markers
markerTool(fs, dest, false, 0);
LOG.info("Auditing destination paths");
verifyRenamed(dest, createdPaths);
}
/**
* Create a FS where only dir2 in the source tree keeps markers;
* verify all is good.
*/
@Test
public void testAuthPathIsMixed() throws Throwable {
describe("Create a source tree with mixed semantics");
Path base = methodPath();
Path source = new Path(base, "source");
Path dest = new Path(base, "dest");
Path dir2 = new Path(source, "dir2");
S3AFileSystem mixedFSDir2 = createFS(DIRECTORY_MARKER_POLICY_AUTHORITATIVE,
dir2.toUri().toString());
// line up for close in teardown
setMixedFS(mixedFSDir2);
// some of these paths will retain markers, some will not
CreatedPaths createdPaths = createPaths(mixedFSDir2, source);
// markers are only under dir2
markerTool(mixedFSDir2, toPath(source, "dir1"), false, 0);
markerTool(mixedFSDir2, source, false, expectedMarkersUnderDir2);
// full scan of source will fail
markerTool(EXIT_NOT_ACCEPTABLE,
mixedFSDir2, source, false, 0, 0, false);
// but add the -nonauth option and the markers under dir2 are skipped
markerTool(0, mixedFSDir2, source, false, 0, 0, true);
// if we now rename, all will be good
LOG.info("Executing rename");
mixedFSDir2.rename(source, dest);
assertIsDirectory(dest);
// there are no markers
MarkerTool.ScanResult scanResult = markerTool(mixedFSDir2, dest, false, 0);
// there are exactly the files we want
Assertions.assertThat(scanResult)
.describedAs("Scan result %s", scanResult)
.extracting(s -> s.getTracker().getFilesFound())
.isEqualTo(expectedFileCount);
verifyRenamed(dest, createdPaths);
}
/**
* Assert that an expected number of markers were deleted.
* @param expected expected count.
* @param result scan result
*/
private static void assertMarkersDeleted(int expected,
MarkerTool.ScanResult result) {
Assertions.assertThat(result.getPurgeSummary())
.describedAs("Purge result of scan %s", result)
.isNotNull()
.extracting(f -> f.getMarkersDeleted())
.isEqualTo(expected);
}
/**
* Marker tool with no args.
*/
@Test
public void testRunNoArgs() throws Throwable {
runToFailure(EXIT_USAGE, MARKERS);
}
@Test
public void testRunWrongBucket() throws Throwable {
runToFailure(EXIT_NOT_FOUND, MARKERS,
AUDIT,
"s3a://this-bucket-does-not-exist-hopefully");
}
/**
* Run with a path that doesn't exist.
*/
@Test
public void testRunUnknownPath() throws Throwable {
runToFailure(EXIT_NOT_FOUND, MARKERS,
AUDIT,
methodPath());
}
/**
* Having both -audit and -clean on the command line is an error.
*/
@Test
public void testRunTooManyActions() throws Throwable {
runToFailure(EXIT_USAGE, MARKERS,
AUDIT, CLEAN,
methodPath());
}
@Test
public void testRunAuditWithExpectedMarkers() throws Throwable {
describe("Run a verbose audit expecting some markers");
// a run under the keeping FS will create paths
CreatedPaths createdPaths = createPaths(getKeepingFS(), methodPath());
final File audit = tempAuditFile();
run(MARKERS, V,
AUDIT,
m(OPT_LIMIT), 0,
m(OPT_OUT), audit,
m(OPT_MIN), expectedMarkersWithBaseDir - 1,
m(OPT_MAX), expectedMarkersWithBaseDir + 1,
createdPaths.base);
expectMarkersInOutput(audit, expectedMarkersWithBaseDir);
}
@Test
public void testRunAuditWithExpectedMarkersSwappedMinMax() throws Throwable {
describe("Run a verbose audit with the min/max ranges swapped;"
+ " see HADOOP-17332");
// a run under the keeping FS will create paths
CreatedPaths createdPaths = createPaths(getKeepingFS(), methodPath());
final File audit = tempAuditFile();
run(MARKERS, V,
AUDIT,
m(OPT_LIMIT), 0,
m(OPT_OUT), audit,
m(OPT_MIN), expectedMarkersWithBaseDir + 1,
m(OPT_MAX), expectedMarkersWithBaseDir - 1,
createdPaths.base);
expectMarkersInOutput(audit, expectedMarkersWithBaseDir);
}
@Test
public void testRunAuditWithExcessMarkers() throws Throwable {
describe("Run a verbose audit failing as surplus markers were found");
// a run under the keeping FS will create paths
CreatedPaths createdPaths = createPaths(getKeepingFS(), methodPath());
final File audit = tempAuditFile();
runToFailure(EXIT_NOT_ACCEPTABLE, MARKERS, V,
AUDIT,
m(OPT_OUT), audit,
createdPaths.base);
expectMarkersInOutput(audit, expectedMarkersWithBaseDir);
}
@Test
public void testRunLimitedAudit() throws Throwable {
describe("Audit with a limited number of files (2)");
CreatedPaths createdPaths = createPaths(getKeepingFS(), methodPath());
runToFailure(EXIT_INTERRUPTED,
MARKERS, V,
m(OPT_LIMIT), 2,
CLEAN,
createdPaths.base);
}
/**
* Run an audit against the landsat bucket.
* <p></p>
* This tests paging/scale against a larger bucket without
* worrying about setup costs.
*/
@Test
public void testRunLimitedLandsatAudit() throws Throwable {
describe("Audit a few thousand landsat objects");
final File audit = tempAuditFile();
runToFailure(EXIT_INTERRUPTED,
MARKERS,
AUDIT,
m(OPT_LIMIT), 3000,
m(OPT_OUT), audit,
LANDSAT_BUCKET);
readOutput(audit);
}
@Test
public void testBucketInfoKeepingOnDeleting() throws Throwable {
describe("Run bucket info with the keeping config on the deleting fs");
runS3GuardCommandToFailure(uncachedFSConfig(getDeletingFS()),
EXIT_NOT_ACCEPTABLE,
BUCKET_INFO,
m(MARKERS), DIRECTORY_MARKER_POLICY_KEEP,
methodPath());
}
@Test
public void testBucketInfoKeepingOnKeeping() throws Throwable {
describe("Run bucket info with the keeping config on the keeping fs");
runS3GuardCommand(uncachedFSConfig(getKeepingFS()),
BUCKET_INFO,
m(MARKERS), DIRECTORY_MARKER_POLICY_KEEP,
methodPath());
}
@Test
public void testBucketInfoDeletingOnDeleting() throws Throwable {
describe("Run bucket info with the deleting config on the deleting fs");
runS3GuardCommand(uncachedFSConfig(getDeletingFS()),
BUCKET_INFO,
m(MARKERS), DIRECTORY_MARKER_POLICY_DELETE,
methodPath());
}
@Test
public void testBucketInfoAuthOnAuth() throws Throwable {
describe("Run bucket info with the auth FS");
Path base = methodPath();
S3AFileSystem authFS = createFS(DIRECTORY_MARKER_POLICY_AUTHORITATIVE,
base.toUri().toString());
// line up for close in teardown
setMixedFS(authFS);
runS3GuardCommand(uncachedFSConfig(authFS),
BUCKET_INFO,
m(MARKERS), DIRECTORY_MARKER_POLICY_AUTHORITATIVE,
methodPath());
}
/**
* Tracker of created paths.
*/
private static final class CreatedPaths {
private final FileSystem fs;
private final Path base;
private List<Path> files = new ArrayList<>();
private List<Path> dirs = new ArrayList<>();
private List<Path> emptyDirs = new ArrayList<>();
private List<String> filesUnderBase = new ArrayList<>();
private List<String> dirsUnderBase = new ArrayList<>();
private List<String> emptyDirsUnderBase = new ArrayList<>();
/**
* Constructor.
* @param fs filesystem.
* @param base base directory for all creation operations.
*/
private CreatedPaths(final FileSystem fs,
final Path base) {
this.fs = fs;
this.base = base;
}
/**
* Make a set of directories.
* @param names varargs list of paths under the base.
* @return number of entries created.
* @throws IOException failure
*/
private int dirs(String... names) throws IOException {
for (String name : names) {
mkdir(name);
}
return names.length;
}
/**
* Create a single directory under the base.
* @param name name/relative names of the directory
* @return the path of the new entry.
*/
private Path mkdir(String name) throws IOException {
Path dir = toPath(base, name);
fs.mkdirs(dir);
dirs.add(dir);
dirsUnderBase.add(name);
return dir;
}
/**
* Make a set of empty directories.
* @param names varargs list of paths under the base.
* @return number of entries created.
* @throws IOException failure
*/
private int emptydirs(String... names) throws IOException {
for (String name : names) {
emptydir(name);
}
return names.length;
}
/**
* Create an empty directory.
* @param name name under the base dir
* @return the path
* @throws IOException failure
*/
private Path emptydir(String name) throws IOException {
Path dir = toPath(base, name);
fs.mkdirs(dir);
emptyDirs.add(dir);
emptyDirsUnderBase.add(name);
return dir;
}
/**
* Make a set of files.
* @param names varargs list of paths under the base.
* @return number of entries created.
* @throws IOException failure
*/
private int files(String... names) throws IOException {
for (String name : names) {
mkfile(name);
}
return names.length;
}
/**
* Create a 0-byte file.
* @param name name under the base dir
* @return the path
* @throws IOException failure
*/
private Path mkfile(String name)
throws IOException {
Path file = toPath(base, name);
ContractTestUtils.touch(fs, file);
files.add(file);
filesUnderBase.add(name);
return file;
}
}
/**
* Create the "standard" test paths.
* @param fs filesystem
* @param base base dir
* @return the details on what was created.
*/
private CreatedPaths createPaths(FileSystem fs, Path base)
throws IOException {
CreatedPaths r = new CreatedPaths(fs, base);
// the directories under which we will create files,
// so expect to have markers
r.mkdir("");
// create the empty dirs
r.emptydir("empty");
// dir 1 has a file underneath
r.mkdir("dir1");
expectedFileCount = r.files("dir1/file1");
expectedMarkersUnderDir1 = 1;
// dir2 has a subdir
r.dirs("dir2", "dir2/dir3");
// an empty subdir
r.emptydir("dir2/empty2");
// and a file under itself and dir3
expectedFileCount += r.files(
"dir2/file2",
"dir2/dir3/file3");
// wrap up the expectations.
expectedMarkersUnderDir2 = 2;
expectedMarkers = expectedMarkersUnderDir1 + expectedMarkersUnderDir2;
expectedMarkersWithBaseDir = expectedMarkers + 1;
return r;
}
/**
* Verify that all the paths renamed from the source exist
* under the destination, including all empty directories.
* @param dest destination to look under.
* @param createdPaths list of created paths.
*/
void verifyRenamed(final Path dest,
final CreatedPaths createdPaths) throws IOException {
// all leaf directories exist
for (String p : createdPaths.emptyDirsUnderBase) {
assertIsDirectory(toPath(dest, p));
}
// non-empty dirs
for (String p : createdPaths.dirsUnderBase) {
assertIsDirectory(toPath(dest, p));
}
// all files exist
for (String p : createdPaths.filesUnderBase) {
assertIsFile(toPath(dest, p));
}
}
}