DockerLocalCommandExecutor.java

/**
 * Copyright (c) 2022, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.computation.local.test;

import com.powsybl.computation.local.LocalCommandExecutor;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Etienne Lesot {@literal <etienne.lesot at rte-france.com>}
 */
public class DockerLocalCommandExecutor implements LocalCommandExecutor {

    private final String dockerImage;
    private final Path hostVolumePath;
    private final Path containerVolumePath;
    private final Map<Path, GenericContainer<?>> containers = new ConcurrentHashMap<>();

    public DockerLocalCommandExecutor(Path hostVolumePath, Path containerVolumePath, ComputationDockerConfig config) {
        this.dockerImage = Objects.requireNonNull(config.getDockerImageId());
        this.hostVolumePath = Objects.requireNonNull(hostVolumePath);
        this.containerVolumePath = Objects.requireNonNull(containerVolumePath);
    }

    @Override
    public int execute(String program, List<String> args, Path outFile, Path errFile, Path workingDir, Map<String, String> env) throws IOException, InterruptedException {
        try (GenericContainer<?> container = new GenericContainer<>(DockerImageName.parse(dockerImage))
                .withFileSystemBind(hostVolumePath.toString(), containerVolumePath.toString(), BindMode.READ_WRITE)
                // some containers are configured to ran by default as a non root user, so we force to always run
                // as root to avoid permission denied on host mounted directory
                .withCreateContainerCmdModifier(cmd -> cmd.withUser("root"))
                .withCommand("sleep", "infinity")) {
            Container.ExecResult execResult;
            containers.put(workingDir, container);
            try {
                env.entrySet().stream().filter(entry -> !entry.getKey().equals("PATH"))
                        .forEach(entry -> container.withEnv(entry.getKey(), entry.getValue()));
                String containerWorkingDir = containerVolumePath.resolve(hostVolumePath.relativize(workingDir)).toString();
                container.withWorkingDirectory(containerWorkingDir);
                List<String> arguments = new ArrayList<>(args);
                arguments.add(0, program);
                container.start();
                execResult = container.execInContainer(arguments.toArray(new String[0]));
                // we need to chmod 777 all created files because of potential directories created by the program
                // executed by the command:
                //  - containerWorkingDir is created by host, so all files created in this directory inside the container
                //  will be deletable byt host
                //  - subdirectories of containerWorkingDir created inside the container WON'T BE DELETABLE BY THE HOST,
                //  so we need to change their permissions
                container.execInContainer("chmod", "-R", "777", containerWorkingDir);
                Files.writeString(errFile, execResult.getStderr());
                Files.writeString(outFile, execResult.getStdout());
                container.stop();
            } finally {
                containers.remove(workingDir);
            }
            return execResult.getExitCode();
        }
    }

    @Override
    public void stop(Path path) {
        GenericContainer<?> container = containers.get(path);
        if (container != null && container.isRunning()) {
            container.stop();
        }
    }

    @Override
    public void stopForcibly(Path path) {
        stop(path);
    }
}