/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.docker.machine.cleaner;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.environment.server.CheEnvironmentEngine;
import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
import org.eclipse.che.commons.schedule.ScheduleRate;
import org.eclipse.che.plugin.docker.client.DockerConnector;
import org.eclipse.che.plugin.docker.client.DockerConnectorProvider;
import org.eclipse.che.plugin.docker.client.json.ContainerListEntry;
import org.eclipse.che.plugin.docker.client.json.Filters;
import org.eclipse.che.plugin.docker.client.json.network.Network;
import org.eclipse.che.plugin.docker.client.params.network.GetNetworksParams;
import org.eclipse.che.plugin.docker.machine.DockerContainerNameGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.che.plugin.docker.client.params.RemoveContainerParams.create;
import static org.eclipse.che.plugin.docker.machine.DockerContainerNameGenerator.ContainerNameInfo;
/**
* Job for periodically clean up abandoned docker containers and networks created by CHE.
* Also, logs active containers list.
*
* @author Alexander Andrienko
* @author Mykola Morhun
*/
@Singleton
public class DockerAbandonedResourcesCleaner implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(DockerAbandonedResourcesCleaner.class);
private static final Filters NETWORK_FILTERS = new Filters().withFilter("type", "custom");
private static final GetNetworksParams GET_NETWORKS_PARAMS = GetNetworksParams.create().withFilters(NETWORK_FILTERS);
private static final String WORKSPACE_ID_REGEX_GROUP = "workspaceId";
private static final String CHE_NETWORK_REGEX = "^(?<" + WORKSPACE_ID_REGEX_GROUP + ">workspace[a-z\\d]{16})_[a-z\\d]{16}$";
private static final Pattern CHE_NETWORK_PATTERN = Pattern.compile(CHE_NETWORK_REGEX);
// TODO replace with WorkspaceManager
private final CheEnvironmentEngine environmentEngine;
private final DockerConnector dockerConnector;
private final DockerContainerNameGenerator nameGenerator;
private final WorkspaceRuntimes runtimes;
private final Set<String> additionalNetworks;
@Inject
public DockerAbandonedResourcesCleaner(CheEnvironmentEngine environmentEngine,
DockerConnectorProvider dockerConnectorProvider,
DockerContainerNameGenerator nameGenerator,
WorkspaceRuntimes workspaceRuntimes,
@Named("machine.docker.networks") Set<Set<String>> additionalNetworks) {
this.environmentEngine = environmentEngine;
this.dockerConnector = dockerConnectorProvider.get();
this.nameGenerator = nameGenerator;
this.runtimes = workspaceRuntimes;
this.additionalNetworks = additionalNetworks.stream()
.flatMap(Set::stream)
.collect(toSet());
}
@ScheduleRate(periodParameterName = "che.docker.cleanup_period_min",
initialDelay = 0L,
unit = TimeUnit.MINUTES)
@Override
public void run() {
cleanContainers();
cleanNetworks();
}
/**
* Cleans up CHE docker containers which don't tracked by API any more.
*/
@VisibleForTesting
void cleanContainers() {
List<String> activeContainers = new ArrayList<>();
try {
for (ContainerListEntry container : dockerConnector.listContainers()) {
String containerName = container.getNames()[0];
Optional<ContainerNameInfo> optional = nameGenerator.parse(containerName);
if (optional.isPresent()) {
try {
// container is orphaned if not found exception is thrown
environmentEngine.getMachine(optional.get().getWorkspaceId(),
optional.get().getMachineId());
activeContainers.add(containerName);
} catch (NotFoundException e) {
cleanUpContainer(container);
} catch (Exception e) {
LOG.error(format("Failed to check activity for container with name '%s'. Cause: %s",
containerName, e.getLocalizedMessage()), e);
}
}
}
} catch (IOException e) {
LOG.error("Failed to get list docker containers", e);
} catch (Exception e) {
LOG.error("Failed to clean up inactive containers", e);
}
LOG.info("List containers registered in the api: " + activeContainers);
}
private void cleanUpContainer(ContainerListEntry container) {
String containerId = container.getId();
String containerName = container.getNames()[0];
killContainer(containerId, containerName, container.getStatus());
removeContainer(containerId, containerName);
}
private void killContainer(String containerId, String containerName, String containerStatus) {
try {
if (containerStatus.startsWith("Up")) {
dockerConnector.killContainer(containerId);
LOG.warn("Unused container with 'id': '{}' and 'name': '{}' was killed ", containerId, containerName);
}
} catch (IOException e) {
LOG.error(format("Failed to kill unused container with 'id': '%s' and 'name': '%s'", containerId, containerName), e);
}
}
private void removeContainer(String containerId, String containerName) {
try {
dockerConnector.removeContainer(create(containerId).withForce(true).withRemoveVolumes(true));
LOG.warn("Unused container with 'id': '{}' and 'name': '{}' was removed", containerId, containerName);
} catch (IOException e) {
LOG.error(format("Failed to delete unused container with 'id': '%s' and 'name': '%s'", containerId, containerName), e);
}
}
/**
* Deletes all abandoned CHE networks.
* Abandoned networks appear when workspaces were stopped unexpectedly,
* for example, force stop che, restart docker, turn off PC, etc.
* A network is considered abandoned when it doesn't contain a container.
* To do this job more efficiently, it should be invoked after cleaning of abandoned containers.
*/
@VisibleForTesting
void cleanNetworks() {
try {
List<Network> customNetworks = dockerConnector.getNetworks(GET_NETWORKS_PARAMS);
// This workaround is added because of docker bug which returns null instead of empty list
// See https://github.com/docker/docker/issues/29946
if (customNetworks == null) {
return;
}
for (Network network : customNetworks) {
Matcher cheNetworkMatcher = CHE_NETWORK_PATTERN.matcher(network.getName());
if (cheNetworkMatcher.matches() && network.getContainers().isEmpty() && !additionalNetworks.contains(network.getName()) &&
!runtimes.hasRuntime(cheNetworkMatcher.group(WORKSPACE_ID_REGEX_GROUP))) {
try {
dockerConnector.removeNetwork(network.getId());
} catch (IOException e) {
LOG.warn("Failed to remove abandoned network: " + network.getName(), e);
}
}
}
} catch (IOException e) {
LOG.error("Failed to get list of docker networks", e);
}
}
}