diff --git a/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/ItemsFromStdin.java b/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/ItemsFromStdin.java index 79bad7ed7328..8d096ab3573f 100644 --- a/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/ItemsFromStdin.java +++ b/hadoop-hdds/cli-common/src/main/java/org/apache/hadoop/hdds/cli/ItemsFromStdin.java @@ -33,9 +33,21 @@ public abstract class ItemsFromStdin implements Iterable { ": one or more, separated by spaces. To read from stdin, specify '-' and supply one item per line."; private List items = new ArrayList<>(); + private boolean readFromStdin = false; protected void setItems(List arguments) { - items = readItemsFromStdinIfNeeded(arguments); + readFromStdin = arguments != null && !arguments.isEmpty() && + "-".equals(arguments.iterator().next()); + + if (readFromStdin) { + items = readItemsFromStdin(); + } else { + items = arguments == null ? new ArrayList<>() : arguments; + } + } + + public boolean isReadFromStdin() { + return readFromStdin; } public List getItems() { @@ -52,11 +64,7 @@ public int size() { return items.size(); } - private static List readItemsFromStdinIfNeeded(List parameters) { - if (parameters.isEmpty() || !"-".equals(parameters.iterator().next())) { - return parameters; - } - + private static List readItemsFromStdin() { List items = new ArrayList<>(); Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8.name()); while (scanner.hasNextLine()) { diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java index c09ba8a2f1d6..acefb5f0b38e 100644 --- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java +++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerInfo.java @@ -20,6 +20,7 @@ import static java.lang.Math.max; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Clock; import java.time.Instant; @@ -87,6 +88,7 @@ public final class ContainerInfo implements Comparable { private long sequenceId; // Health state of the container (determined by ReplicationManager) private ContainerHealthState healthState; + private boolean suppressed; private ContainerInfo(Builder b) { containerID = ContainerID.valueOf(b.containerID); @@ -102,6 +104,7 @@ private ContainerInfo(Builder b) { replicationConfig = b.replicationConfig; clock = b.clock; healthState = b.healthState != null ? b.healthState : ContainerHealthState.HEALTHY; + suppressed = b.suppressed; } public static Codec getCodec() { @@ -123,6 +126,10 @@ public static ContainerInfo fromProtobuf(HddsProtos.ContainerInfoProto info) { .setReplicationConfig(config) .setSequenceId(info.getSequenceId()); + if (info.hasSuppressed()) { + builder.setSuppressed(info.getSuppressed()); + } + if (info.hasPipelineID()) { builder.setPipelineID(PipelineID.getFromProtobuf(info.getPipelineID())); } @@ -263,6 +270,26 @@ public void setHealthState(ContainerHealthState newHealthState) { this.healthState = newHealthState; } + /** + * Check if container is suppressed. + * Only included in JSON output when true. + * + * @return boolean + */ + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + public boolean isSuppressed() { + return suppressed; + } + + /** + * Set the boolean for suppressed. + * + * @param suppressed checks if container is suppressed or not + */ + public void setSuppressed(boolean suppressed) { + this.suppressed = suppressed; + } + @JsonIgnore public HddsProtos.ContainerInfoProto getProtobuf() { HddsProtos.ContainerInfoProto.Builder builder = @@ -288,6 +315,10 @@ public HddsProtos.ContainerInfoProto getProtobuf() { builder.setPipelineID(getPipelineID().getProtobuf()); } + if (suppressed) { + builder.setSuppressed(true); + } + return builder.build(); } @@ -390,6 +421,7 @@ public static class Builder { private PipelineID pipelineID; private ReplicationConfig replicationConfig; private ContainerHealthState healthState; + private boolean suppressed; public Builder setPipelineID(PipelineID pipelineId) { this.pipelineID = pipelineId; @@ -447,6 +479,11 @@ public Builder setHealthState(ContainerHealthState healthState) { return this; } + public Builder setSuppressed(boolean suppressed) { + this.suppressed = suppressed; + return this; + } + /** * Also resets {@code stateEnterTime}, so make sure to set clock first. */ diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java index fac1467dd8f2..cb4b8471a00c 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/client/ScmClient.java @@ -145,6 +145,25 @@ ContainerListResult listContainer(long startContainerID, int count, ReplicationConfig replicationConfig) throws IOException; + /** + * Lists a range of containers and get their info. + * + * @param startContainerID start containerID. + * @param count count must be {@literal >} 0. + * @param state Container of this state will be returned. + * @param replicationConfig container replication Config. + * @param suppressed container to be suppressed/unsuppressed from report + * @return a list of containers capped by max count allowed + * in "ozone.scm.container.list.max.count" and total number of containers. + * @throws IOException + */ + ContainerListResult listContainer(long startContainerID, int count, + HddsProtos.LifeCycleState state, + HddsProtos.ReplicationType replicationType, + ReplicationConfig replicationConfig, + Boolean suppressed) + throws IOException; + /** * Read meta data from an existing container. * @param containerID - ID of the container. @@ -465,4 +484,14 @@ DecommissionScmResponseProto decommissionScm( */ void reconcileContainer(long containerID) throws IOException; + /** + * Suppress or unsuppress containers from reports. + * Suppressed containers are excluded from replication manager reports + * regardless of their health state. + * + * @param containerIds container IDs to suppress or unsuppress + * @param suppress true to suppress, false to unsuppress + * @throws IOException + */ + List suppressContainers(List containerIds, boolean suppress) throws IOException; } diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java index 32da41f8f17d..98f8efa9ae30 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocol.java @@ -228,6 +228,30 @@ ContainerListResult listContainer(long startContainerID, HddsProtos.ReplicationType replicationType, ReplicationConfig replicationConfig) throws IOException; + /** + * Ask SCM for a list of containers with a range of container ID, state + * and replication config, and the limit of count. + * The containers are returned from startID (exclusive), and + * filtered by state and replication config. The returned list is limited to + * count entries. + * + * @param startContainerID start container ID. + * @param count count, if count {@literal <} 0, the max size is unlimited.( + * Usually the count will be replace with a very big + * value instead of being unlimited in case the db is very big) + * @param state Container with this state will be returned. + * @param replicationConfig Replication config for the containers + * @param suppressed container to be suppressed/unsuppressed from report + * @return a list of containers capped by max count allowed + * in "ozone.scm.container.list.max.count" and total number of containers. + * @throws IOException + */ + ContainerListResult listContainer(long startContainerID, + int count, HddsProtos.LifeCycleState state, + HddsProtos.ReplicationType replicationType, + ReplicationConfig replicationConfig, + Boolean suppressed) throws IOException; + /** * Deletes a container in SCM. * @@ -521,4 +545,13 @@ DecommissionScmResponseProto decommissionScm( * @throws IOException On error */ void reconcileContainer(long containerID) throws IOException; + + /** + * Suppress or unsuppress containers from reports. + * + * @param containerIds container IDs to suppress or unsuppress + * @param suppress true to suppress, false to unsuppress + * @throws IOException + */ + List suppressContainers(List containerIds, boolean suppress) throws IOException; } diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java index ca9424657700..56e2ef6408fc 100644 --- a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java +++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java @@ -127,6 +127,8 @@ import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.StartReplicationManagerRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.StopContainerBalancerRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.StopReplicationManagerRequestProto; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.SuppressContainerRequestProto; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.SuppressContainerResponseProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.Type; import org.apache.hadoop.hdds.scm.DatanodeAdminError; import org.apache.hadoop.hdds.scm.ScmInfo; @@ -445,6 +447,16 @@ public ContainerListResult listContainer(long startContainerID, int count, HddsProtos.ReplicationType replicationType, ReplicationConfig replicationConfig) throws IOException { + return listContainer(startContainerID, count, state, replicationType, replicationConfig, null); + } + + @Override + public ContainerListResult listContainer(long startContainerID, int count, + HddsProtos.LifeCycleState state, + HddsProtos.ReplicationType replicationType, + ReplicationConfig replicationConfig, + Boolean suppressed) + throws IOException { Preconditions.checkState(startContainerID >= 0, "Container ID cannot be negative."); Preconditions.checkState(count > 0, @@ -454,6 +466,9 @@ public ContainerListResult listContainer(long startContainerID, int count, builder.setStartContainerID(startContainerID); builder.setCount(count); builder.setTraceID(TracingUtil.exportCurrentSpan()); + if (suppressed != null) { + builder.setSuppressed(suppressed); + } if (state != null) { builder.setState(state); } @@ -1312,6 +1327,19 @@ public void reconcileContainer(long containerID) throws IOException { submitRequest(Type.ReconcileContainer, builder -> builder.setReconcileContainerRequest(request)); } + @Override + public List suppressContainers(List containerIds, boolean suppress) + throws IOException { + SuppressContainerRequestProto request = SuppressContainerRequestProto.newBuilder() + .addAllContainerIDs(containerIds) + .setSuppress(suppress) + .build(); + SuppressContainerResponseProto response = + submitRequest(Type.SuppressContainer, builder -> builder.setSuppressContainerRequest(request)) + .getSuppressContainerResponse(); + return response.getFailedContainerIDsList(); + } + /** * Holder class to store the target SCM node ID for routing requests. * This allows requests to be directed to specific SCM nodes in an HA cluster. diff --git a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto index b6508ca9688d..933bb4a00870 100644 --- a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto +++ b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto @@ -88,6 +88,7 @@ message ScmContainerLocationRequest { optional ReconcileContainerRequestProto reconcileContainerRequest = 49; optional GetDeletedBlocksTxnSummaryRequestProto getDeletedBlocksTxnSummaryRequest = 50; optional SCMListContainerIDsRequestProto scmListContainerIDsRequest = 51; + optional SuppressContainerRequestProto suppressContainerRequest = 52; } message ScmContainerLocationResponse { @@ -147,6 +148,7 @@ message ScmContainerLocationResponse { optional ReconcileContainerResponseProto reconcileContainerResponse = 49; optional GetDeletedBlocksTxnSummaryResponseProto getDeletedBlocksTxnSummaryResponse = 50; optional SCMListContainerIDsResponseProto scmListContainerIDsResponse = 51; + optional SuppressContainerResponseProto suppressContainerResponse = 52; enum Status { OK = 1; @@ -205,6 +207,7 @@ enum Type { ReconcileContainer = 45; GetDeletedBlocksTransactionSummary = 46; ListContainerIDs = 47; + SuppressContainer = 48; } /** @@ -313,6 +316,7 @@ message SCMListContainerRequestProto { optional ReplicationFactor factor = 5; optional ReplicationType type = 6; optional ECReplicationConfig ecReplicationConfig = 7; + optional bool suppressed = 8; } message SCMListContainerResponseProto { @@ -711,6 +715,16 @@ message ReconcileContainerRequestProto { message ReconcileContainerResponseProto { } +message SuppressContainerRequestProto { + repeated int64 containerIDs = 1; + optional bool suppress = 2; +} + +message SuppressContainerResponseProto { + // Container IDs that failed to suppress or unsuppress. Empty if all succeeded. + repeated int64 failedContainerIDs = 1; +} + /** * Protocol used from an HDFS node to StorageContainerManager. See the request * and response messages for details of the RPC calls. diff --git a/hadoop-hdds/interface-client/src/main/proto/hdds.proto b/hadoop-hdds/interface-client/src/main/proto/hdds.proto index 2f20fde3e093..eab367559273 100644 --- a/hadoop-hdds/interface-client/src/main/proto/hdds.proto +++ b/hadoop-hdds/interface-client/src/main/proto/hdds.proto @@ -271,6 +271,7 @@ message ContainerInfoProto { optional ReplicationFactor replicationFactor = 10; required ReplicationType replicationType = 11; optional ECReplicationConfig ecReplicationConfig = 12; + optional bool suppressed = 13; } message ContainerWithPipeline { diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManager.java index 3c5706cc0fb9..691a1965ab84 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManager.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import org.apache.hadoop.hdds.client.ReplicationConfig; +import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ContainerInfoProto; import org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleEvent; import org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState; import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationType; @@ -238,4 +239,14 @@ void deleteContainer(ContainerID containerID) * @return containerStateManger */ ContainerStateManager getContainerStateManager(); + + /** + * Update container info in the container manager. + * This is used for updating container metadata like ackMissing flag. + * + * @param containerInfo Updated container info proto + * @throws IOException + */ + void updateContainerInfo(ContainerID containerID, ContainerInfoProto containerInfo) + throws IOException; } diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManagerImpl.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManagerImpl.java index 432c9890e98a..7780c0839462 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManagerImpl.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerManagerImpl.java @@ -302,6 +302,21 @@ public void updateContainerState(final ContainerID cid, } } + @Override + public void updateContainerInfo(final ContainerID cid, ContainerInfoProto containerInfo) + throws IOException { + lock.lock(); + try { + if (containerExist(cid)) { + containerStateManager.updateContainerInfo(containerInfo); + } else { + throw new ContainerNotFoundException(cid); + } + } finally { + lock.unlock(); + } + } + @Override public void transitionDeletingOrDeletedToTargetState(ContainerID containerID, LifeCycleState targetState) throws IOException { diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManager.java index 0d66027480d2..d89fc92a8f4c 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManager.java @@ -219,4 +219,14 @@ void removeContainer(HddsProtos.ContainerID containerInfo) */ void reinitialize(Table containerStore) throws IOException; + + /** + * Update container info. + * + * @param containerInfo Updated container info proto + * @throws IOException + */ + @Replicate + void updateContainerInfo(HddsProtos.ContainerInfoProto containerInfo) + throws IOException; } diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManagerImpl.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManagerImpl.java index 5c95aff5c190..48c1adedd7d6 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManagerImpl.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ContainerStateManagerImpl.java @@ -564,6 +564,24 @@ public void reinitialize( } } + @Override + public void updateContainerInfo(HddsProtos.ContainerInfoProto updatedInfoProto) + throws IOException { + ContainerInfo updatedInfo = ContainerInfo.fromProtobuf(updatedInfoProto); + ContainerID containerID = updatedInfo.containerID(); + + try (AutoCloseableLock ignored = writeLock(containerID)) { + final ContainerInfo currentInfo = containers.getContainerInfo(containerID); + if (currentInfo == null) { + throw new ContainerNotFoundException(containerID); + } + // Update suppressed flag + currentInfo.setSuppressed(updatedInfo.isSuppressed()); + transactionBuffer.addToBuffer(containerStore, containerID, currentInfo); + LOG.debug("Updated container info for container: {}, suppressed={}", containerID, currentInfo.isSuppressed()); + } + } + private AutoCloseableLock readLock() { return AutoCloseableLock.acquire(lock.readLock()); } diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java index 7096c18e6c5d..8cd8444d1d2f 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/replication/ReplicationManager.java @@ -867,6 +867,12 @@ protected boolean processContainer(ContainerInfo containerInfo, ReplicationQueue repQueue, ReplicationManagerReport report, boolean readOnly) throws ContainerNotFoundException { synchronized (containerInfo) { + // Filter out suppressed containers early + if (containerInfo.isSuppressed()) { + LOG.debug("Skipping suppressed container: {}", containerInfo.getContainerID()); + return false; + } + // Reset health state to HEALTHY before processing this container report.resetContainerHealthState(); final boolean isEC = isEC(containerInfo.getReplicationConfig()); diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java index dd18ad68d13a..73bf92e9cd58 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/protocol/StorageContainerLocationProtocolServerSideTranslatorPB.java @@ -137,6 +137,8 @@ import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.StopContainerBalancerResponseProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.StopReplicationManagerRequestProto; import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.StopReplicationManagerResponseProto; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.SuppressContainerRequestProto; +import org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.SuppressContainerResponseProto; import org.apache.hadoop.hdds.scm.DatanodeAdminError; import org.apache.hadoop.hdds.scm.ScmInfo; import org.apache.hadoop.hdds.scm.container.ContainerID; @@ -760,6 +762,12 @@ public ScmContainerLocationResponse processRequest( .setStatus(Status.OK) .setScmListContainerIDsResponse(listContainerIDs(request.getScmListContainerIDsRequest())) .build(); + case SuppressContainer: + return ScmContainerLocationResponse.newBuilder() + .setCmdType(request.getCmdType()) + .setStatus(Status.OK) + .setSuppressContainerResponse(suppressContainer(request.getSuppressContainerRequest())) + .build(); default: throw new IllegalArgumentException( "Unknown command type: " + request.getCmdType()); @@ -889,6 +897,9 @@ public SCMListContainerResponseProto listContainer( } else if (request.hasFactor()) { factor = request.getFactor(); } + // Filter by suppressed: true (suppressed only), false (unsuppressed only) or null (display all). + Boolean suppressed = request.hasSuppressed() ? request.getSuppressed() : null; + ContainerListResult containerListAndTotalCount; if (factor != null) { // Call from a legacy client @@ -896,7 +907,7 @@ public SCMListContainerResponseProto listContainer( impl.listContainer(startContainerID, count, state, factor); } else { containerListAndTotalCount = - impl.listContainer(startContainerID, count, state, replicationType, repConfig); + impl.listContainer(startContainerID, count, state, replicationType, repConfig, suppressed); } SCMListContainerResponseProto.Builder builder = SCMListContainerResponseProto.newBuilder(); @@ -1434,4 +1445,11 @@ public SCMListContainerIDsResponseProto listContainerIDs( return builder.build(); } + + public SuppressContainerResponseProto suppressContainer(SuppressContainerRequestProto request) throws IOException { + List failedContainerIDs = impl.suppressContainers(request.getContainerIDsList(), request.getSuppress()); + return SuppressContainerResponseProto.newBuilder() + .addAllFailedContainerIDs(failedContainerIDs) + .build(); + } } diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java index b161e0e84d76..1217215734de 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java @@ -451,7 +451,7 @@ private boolean hasRequiredReplicas(ContainerInfo contInfo) { @Override public ContainerListResult listContainer(long startContainerID, int count) throws IOException { - return listContainer(startContainerID, count, null, null, null); + return listContainer(startContainerID, count, null, null, null, null); } /** @@ -468,7 +468,7 @@ public ContainerListResult listContainer(long startContainerID, @Override public ContainerListResult listContainer(long startContainerID, int count, HddsProtos.LifeCycleState state) throws IOException { - return listContainer(startContainerID, count, state, null, null); + return listContainer(startContainerID, count, state, null, null, null); } /** @@ -487,20 +487,28 @@ public ContainerListResult listContainer(long startContainerID, public ContainerListResult listContainer(long startContainerID, int count, HddsProtos.LifeCycleState state, HddsProtos.ReplicationFactor factor) throws IOException { - return listContainerInternal(startContainerID, count, state, factor, null, null); + return listContainerInternal(startContainerID, count, state, factor, null, null, null); } private ContainerListResult listContainerInternal(long startContainerID, int count, HddsProtos.LifeCycleState state, HddsProtos.ReplicationFactor factor, HddsProtos.ReplicationType replicationType, - ReplicationConfig repConfig) throws IOException { + ReplicationConfig repConfig, + Boolean suppressed) throws IOException { boolean auditSuccess = true; - Map auditMap = buildAuditMap(startContainerID, count, state, factor, replicationType, repConfig); + Map auditMap = buildAuditMap(startContainerID, count, state, factor, + replicationType, repConfig, suppressed); try { Stream containerStream = buildContainerStream(factor, replicationType, repConfig, getBaseContainerStream(state)); + + // Filter by suppressed flag only if explicitly specified + if (suppressed != null) { + containerStream = containerStream.filter(info -> info.isSuppressed() == suppressed); + } + List containerInfos = containerStream.filter(info -> info.containerID().getId() >= startContainerID) .sorted().collect(Collectors.toList()); @@ -552,7 +560,8 @@ private Map buildAuditMap(long startContainerID, int count, HddsProtos.LifeCycleState state, HddsProtos.ReplicationFactor factor, HddsProtos.ReplicationType replicationType, - ReplicationConfig repConfig) { + ReplicationConfig repConfig, + Boolean suppressed) { Map auditMap = new HashMap<>(); auditMap.put("startContainerID", String.valueOf(startContainerID)); auditMap.put("count", String.valueOf(count)); @@ -568,6 +577,9 @@ private Map buildAuditMap(long startContainerID, int count, if (repConfig != null) { auditMap.put("replicationConfig", repConfig.toString()); } + if (suppressed != null) { + auditMap.put("suppressed", suppressed.toString()); + } return auditMap; } @@ -588,7 +600,28 @@ public ContainerListResult listContainer(long startContainerID, int count, HddsProtos.LifeCycleState state, HddsProtos.ReplicationType replicationType, ReplicationConfig repConfig) throws IOException { - return listContainerInternal(startContainerID, count, state, null, replicationType, repConfig); + return listContainerInternal(startContainerID, count, state, null, replicationType, repConfig, null); + } + + /** + * Lists a range of containers and get their info. + * + * @param startContainerID start containerID. + * @param count count must be {@literal >} 0. + * @param state Container with this state will be returned. + * @param repConfig Replication Config for the container. + * @param suppressed container to be suppressed/unsuppressed from report + * @return a list of containers capped by max count allowed + * in "ozone.scm.container.list.max.count" and total number of containers. + * @throws IOException + */ + @Override + public ContainerListResult listContainer(long startContainerID, + int count, HddsProtos.LifeCycleState state, + HddsProtos.ReplicationType replicationType, + ReplicationConfig repConfig, + Boolean suppressed) throws IOException { + return listContainerInternal(startContainerID, count, state, null, replicationType, repConfig, suppressed); } @Override @@ -1695,4 +1728,36 @@ public void reconcileContainer(long longContainerID) throws IOException { throw ex; } } + + @Override + public List suppressContainers(List containerIds, boolean suppress) throws IOException { + getScm().checkAdminAccess(getRemoteUser(), false); + SCMAction action = suppress ? SCMAction.SUPPRESS_CONTAINER : SCMAction.UNSUPPRESS_CONTAINER; + List failedContainerIDs = new ArrayList<>(); + for (long containerId : containerIds) { + try { + persistContainerSuppression(containerId, suppress, action); + } catch (IOException ex) { + failedContainerIDs.add(containerId); + } + } + return failedContainerIDs; + } + + private void persistContainerSuppression(long longContainerID, boolean suppress, SCMAction action) + throws IOException { + ContainerID containerID = ContainerID.valueOf(longContainerID); + final Map auditMap = new HashMap<>(); + auditMap.put("containerID", containerID.toString()); + auditMap.put("suppress", String.valueOf(suppress)); + try { + ContainerInfo containerInfo = scm.getContainerManager().getContainer(containerID); + containerInfo.setSuppressed(suppress); + scm.getContainerManager().updateContainerInfo(containerID, containerInfo.getProtobuf()); + AUDIT.logWriteSuccess(buildAuditMessageForSuccess(action, auditMap)); + } catch (IOException ex) { + AUDIT.logWriteFailure(buildAuditMessageForFailure(action, auditMap, ex)); + throw ex; + } + } } diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java index b7acb40d7ac8..61d536e599f4 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/ozone/audit/SCMAction.java @@ -69,7 +69,9 @@ public enum SCMAction implements AuditAction { GET_PIPELINE, RECONCILE_CONTAINER, GET_DELETED_BLOCK_SUMMARY, - LIST_CONTAINER_IDS; + LIST_CONTAINER_IDS, + SUPPRESS_CONTAINER, + UNSUPPRESS_CONTAINER; @Override public String getAction() { diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java index 18950053e444..c1973891d0aa 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerOperationClient.java @@ -389,6 +389,22 @@ public ContainerListResult listContainer(long startContainerID, startContainerID, count, state, repType, replicationConfig); } + @Override + public ContainerListResult listContainer(long startContainerID, + int count, HddsProtos.LifeCycleState state, + HddsProtos.ReplicationType repType, + ReplicationConfig replicationConfig, + Boolean suppressed) throws IOException { + if (count > maxCountOfContainerList) { + LOG.warn("Attempting to list {} containers. However, this exceeds" + + " the cluster's current limit of {}. The results will be capped at the" + + " maximum allowed count.", count, maxCountOfContainerList); + count = maxCountOfContainerList; + } + return storageContainerLocationClient.listContainer( + startContainerID, count, state, repType, replicationConfig, suppressed); + } + @Override public ContainerDataProto readContainer(long containerID, Pipeline pipeline) throws IOException { @@ -614,4 +630,9 @@ public String getMetrics(String query) throws IOException { public void reconcileContainer(long id) throws IOException { storageContainerLocationClient.reconcileContainer(id); } + + @Override + public List suppressContainers(List containerIds, boolean suppress) throws IOException { + return storageContainerLocationClient.suppressContainers(containerIds, suppress); + } } diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java index 36e615a5829e..a201cc2ab9fe 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ContainerIDParameters.java @@ -18,6 +18,7 @@ package org.apache.hadoop.hdds.scm.cli.container; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import org.apache.hadoop.hdds.cli.ItemsFromStdin; import picocli.CommandLine; @@ -30,13 +31,22 @@ public class ContainerIDParameters extends ItemsFromStdin { private CommandLine.Model.CommandSpec spec; @CommandLine.Parameters(description = "Container IDs" + FORMAT_DESCRIPTION, - arity = "1..*", + arity = "0..*", paramLabel = "") public void setContainerIDs(List arguments) { setItems(arguments); } public List getValidatedIDs() { + return getValidatedIDs(true); + } + + public List getValidatedIDs(boolean required) { + if (required && size() == 0 && !isReadFromStdin()) { + throw new CommandLine.MissingParameterException(spec.commandLine(), + spec.commandLine().getCommandSpec().args(), + "Missing required parameter: ''"); + } List containerIDs = new ArrayList<>(size()); List invalidIDs = new ArrayList<>(); @@ -62,6 +72,6 @@ public List getValidatedIDs() { throw new CommandLine.ParameterException(spec.commandLine(), "Container IDs must be positive integers. Invalid container IDs: " + String.join(" ", invalidIDs)); } - return containerIDs; + return new ArrayList<>(new LinkedHashSet<>(containerIDs)); } } diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java index 4bc9af843a08..0cc4b8bdee1c 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ListSubcommand.java @@ -86,6 +86,11 @@ public class ListSubcommand extends ScmSubcommand { "rs-6-3-1024k for EC)") private String replication; + @Option(names = {"--suppressed"}, + description = "Filter by suppression status. Set to 'true' to list only suppressed containers " + + "or 'false' to list only those that are not suppressed.") + private Boolean suppressed; + private static final ObjectWriter WRITER; static { @@ -129,7 +134,7 @@ public void execute(ScmClient scmClient) throws IOException { } ContainerListResult containerListResult = - scmClient.listContainer(startId, count, state, type, repConfig); + scmClient.listContainer(startId, count, state, type, repConfig, suppressed); writeContainers(sequenceWriter, containerListResult.getContainerInfoList()); @@ -169,7 +174,7 @@ private void listAllContainers(ScmClient scmClient, SequenceWriter writer, do { ContainerListResult result = - scmClient.listContainer(currentStartId, batchSize, state, type, repConfig); + scmClient.listContainer(currentStartId, batchSize, state, type, repConfig, suppressed); fetchedCount = result.getContainerInfoList().size(); writeContainers(writer, result.getContainerInfoList()); diff --git a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReportSubcommand.java b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReportSubcommand.java index b5dc960d8c75..737ed248dd76 100644 --- a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReportSubcommand.java +++ b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/container/ReportSubcommand.java @@ -44,13 +44,64 @@ public class ReportSubcommand extends ScmSubcommand { @CommandLine.Spec private CommandLine.Model.CommandSpec spec; + @CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") + private SuppressOptions suppressOptions; + + @CommandLine.Mixin + private ContainerIDParameters containerList; + @CommandLine.Option(names = { "--json" }, defaultValue = "false", description = "Format output as JSON") private boolean json; + static class SuppressOptions { + @CommandLine.Option(names = {"--suppress"}, + description = "Suppress container(s) from future reports") + private boolean suppress; + + @CommandLine.Option(names = {"--unsuppress"}, + description = "Unsuppress container(s) to include in future reports") + private boolean unsuppress; + } + @Override public void execute(ScmClient scmClient) throws IOException { + if (suppressOptions != null) { + handleSuppressUnsuppress(scmClient); + return; + } + + printReport(scmClient); + } + + private void handleSuppressUnsuppress(ScmClient scmClient) throws IOException { + boolean suppress = suppressOptions.suppress; + List containerIDs = containerList.getValidatedIDs(); + List failedContainerIDs = scmClient.suppressContainers(containerIDs, suppress); + + int failures = 0; + for (long id : containerIDs) { + if (failedContainerIDs.contains(id)) { + err().println("Failed to " + (suppress ? "suppress" : "unsuppress") + " container " + id + "."); + failures++; + } else { + out().println((suppress ? "Suppressed" : "Unsuppressed") + " container: " + id); + } + } + + int numOfSuccess = containerIDs.size() - failures; + if (numOfSuccess > 0) { + out().println("\n" + (suppress ? "Suppressed " : "Unsuppressed ") + numOfSuccess + " container(s) successfully."); + out().println("Container report will be updated after the next Replication Manager cycle."); + } + + if (failures > 0) { + throw new IOException("Failed to " + (suppress ? "suppress " : "unsuppress ") + failures + " container(s)."); + } + } + + private void printReport(ScmClient scmClient) throws IOException { ReplicationManagerReport report = scmClient.getReplicationManagerReport(); if (report.getReportTimeStamp() == 0) { System.err.println("The Container Report is not available until Replication Manager completes" + diff --git a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestContainerReportSuppressOptions.java b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestContainerReportSuppressOptions.java new file mode 100644 index 000000000000..c315847ee237 --- /dev/null +++ b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/container/TestContainerReportSuppressOptions.java @@ -0,0 +1,294 @@ +/* + * 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.hdds.scm.cli.container; + +import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState.CLOSED; +import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.apache.hadoop.hdds.client.RatisReplicationConfig; +import org.apache.hadoop.hdds.protocol.DatanodeID; +import org.apache.hadoop.hdds.protocol.MockDatanodeDetails; +import org.apache.hadoop.hdds.scm.client.ScmClient; +import org.apache.hadoop.hdds.scm.container.ContainerHealthState; +import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ContainerListResult; +import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport; +import org.apache.hadoop.hdds.scm.pipeline.PipelineID; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import picocli.CommandLine; + +/** + * Tests the suppress/unsuppress options in container report. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TestContainerReportSuppressOptions { + + private ScmClient scmClient; + private List containers; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name(); + + @BeforeEach + public void setup() throws IOException { + scmClient = mock(ScmClient.class); + MockDatanodeDetails.createDatanodeDetails(DatanodeID.randomID()); + containers = new ArrayList<>(); + + containers.add(createEmptyContainer(1L)); // EMPTY container (0 keys, no replicas) + containers.add(createMissingContainer(2L)); // MISSING container (has keys, no replicas) + + // Mock RM report + when(scmClient.getReplicationManagerReport()).thenAnswer(inv -> createMockReport()); + + // Mock listContainer + when(scmClient.listContainer(anyLong(), anyInt(), eq(null), eq(null), eq(null), eq(true))) + .thenAnswer(inv -> listSuppressedContainers()); + when(scmClient.listContainer(anyLong(), anyInt(), eq(null), eq(null), eq(null), eq(false))) + .thenAnswer(inv -> listNonSuppressedContainers()); + + // Mock suppress/unsuppress + doReturn(Collections.emptyList()).when(scmClient).suppressContainers(anyList(), eq(true)); + doReturn(Collections.emptyList()).when(scmClient).suppressContainers(anyList(), eq(false)); + + System.setOut(new PrintStream(outContent, false, DEFAULT_ENCODING)); + System.setErr(new PrintStream(errContent, false, DEFAULT_ENCODING)); + } + + @AfterEach + public void tearDown() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + /** + * Test container report shows empty and missing containers. + */ + @Test + @Order(1) + public void testReportShowsEmptyAndMissingContainers() throws IOException { + ReportSubcommand reportCmd = new ReportSubcommand(); + CommandLine c = new CommandLine(reportCmd); + c.parseArgs(); + reportCmd.execute(scmClient); + + String output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("EMPTY: 1")); + assertTrue(output.contains("MISSING: 1")); + } + + /** + * Test suppress missing container and check report. + */ + @Test + @Order(2) + public void testSuppressMissingContainer() throws IOException { + outContent.reset(); + ReportSubcommand reportCmd = new ReportSubcommand(); + CommandLine c = new CommandLine(reportCmd); + c.parseArgs("--suppress", "2"); + reportCmd.execute(scmClient); + + String output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("Suppressed container: 2")); + + containers.get(1).setSuppressed(true); + + outContent.reset(); + reportCmd = new ReportSubcommand(); + c = new CommandLine(reportCmd); + c.parseArgs(); + reportCmd.execute(scmClient); + + output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("EMPTY: 1")); + assertTrue(output.contains("MISSING: 0")); + } + + /** + * Test list suppressed containers from container list command. + */ + @Test + @Order(3) + public void testListSuppressedContainers() throws IOException { + containers.get(1).setSuppressed(true); + + ListSubcommand listCmd = new ListSubcommand(); + CommandLine c = new CommandLine(listCmd); + c.parseArgs("--suppressed=true"); + + outContent.reset(); + listCmd.execute(scmClient); + + String output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("\"containerID\" : 2")); + assertFalse(output.contains("\"containerID\" : 1")); + } + + /** + * Test unsuppress missing container and check report. + */ + @Test + @Order(4) + public void testUnsuppressContainer() throws IOException { + containers.get(1).setSuppressed(true); + + outContent.reset(); + ReportSubcommand reportCmd = new ReportSubcommand(); + CommandLine c = new CommandLine(reportCmd); + c.parseArgs("--unsuppress", "2"); + reportCmd.execute(scmClient); + + String output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("Unsuppressed container: 2")); + + containers.get(1).setSuppressed(false); + + outContent.reset(); + reportCmd = new ReportSubcommand(); + c = new CommandLine(reportCmd); + c.parseArgs(); + reportCmd.execute(scmClient); + + output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("MISSING: 1")); + } + + /** + * Test list unsuppressed containers from container list command. + */ + @Test + @Order(5) + public void testListNonSuppressedContainers() throws IOException { + containers.get(1).setSuppressed(false); + + // List only non-suppressed containers + ListSubcommand listCmd = new ListSubcommand(); + CommandLine c = new CommandLine(listCmd); + c.parseArgs("--suppressed=false"); + + outContent.reset(); + listCmd.execute(scmClient); + + String output = outContent.toString(DEFAULT_ENCODING); + assertTrue(output.contains("\"containerID\" : 1")); + assertTrue(output.contains("\"containerID\" : 2")); + assertFalse(output.contains("\"suppressed\" : true")); + } + + /** + * Create mock RM report based on current container states. + */ + private ReplicationManagerReport createMockReport() { + ReplicationManagerReport report = new ReplicationManagerReport(100); + + for (ContainerInfo container : containers) { + if (container.isSuppressed()) { + // Suppressed containers are filtered from the report + continue; + } + + // Determine health state based on container properties + ContainerHealthState healthState; + if (container.getNumberOfKeys() == 0) { + healthState = ContainerHealthState.EMPTY; + report.incrementAndSample(healthState, container); + } else { + healthState = ContainerHealthState.MISSING; + report.incrementAndSample(healthState, container); + } + + // Also increment lifecycle state + report.increment(container.getState()); + } + + report.setComplete(); + return report; + } + + /** + * Create an EMPTY container (0 keys, no replicas). + */ + private ContainerInfo createEmptyContainer(long containerID) { + return new ContainerInfo.Builder() + .setContainerID(containerID) + .setReplicationConfig(RatisReplicationConfig.getInstance(ONE)) + .setState(CLOSED) + .setOwner("TestOwner") + .setNumberOfKeys(0) // Empty - 0 keys + .setPipelineID(PipelineID.randomId()) + .build(); + } + + /** + * Create a MISSING container (has keys but no replicas). + */ + private ContainerInfo createMissingContainer(long containerID) { + return new ContainerInfo.Builder() + .setContainerID(containerID) + .setReplicationConfig(RatisReplicationConfig.getInstance(ONE)) + .setState(CLOSED) + .setOwner("TestOwner") + .setNumberOfKeys(100) // Has keys + .setPipelineID(PipelineID.randomId()) + .build(); + } + + private ContainerListResult listSuppressedContainers() { + List suppressed = new ArrayList<>(); + for (ContainerInfo container : containers) { + if (container.isSuppressed()) { + suppressed.add(container); + } + } + return new ContainerListResult(suppressed, suppressed.size()); + } + + private ContainerListResult listNonSuppressedContainers() { + List nonSuppressed = new ArrayList<>(); + for (ContainerInfo container : containers) { + if (!container.isSuppressed()) { + nonSuppressed.add(container); + } + } + return new ContainerListResult(nonSuppressed, nonSuppressed.size()); + } +}