/**
 * 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.yarn.server.nodemanager;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.fs.FileContext;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.yarn.api.records.ContainerId;
import org.apache.hadoop.yarn.api.records.ContainerLaunchContext;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container;
import org.apache.hadoop.yarn.server.nodemanager.executor.ContainerStartContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Mock tests for docker container executor
 */
public class TestDockerContainerExecutorWithMocks {

  private static final Log LOG = LogFactory
    .getLog(TestDockerContainerExecutorWithMocks.class);
  public static final String DOCKER_LAUNCH_COMMAND = "/bin/true";
  private DockerContainerExecutor dockerContainerExecutor = null;
  private LocalDirsHandlerService dirsHandler;
  private Path workDir;
  private FileContext lfs;
  private String yarnImage;

  @Before
  public void setup() {
    assumeTrue(Shell.LINUX);
    File f = new File("./src/test/resources/mock-container-executor");
    if(!FileUtil.canExecute(f)) {
      FileUtil.setExecutable(f, true);
    }
    String executorPath = f.getAbsolutePath();
    Configuration conf = new Configuration();
    yarnImage = "yarnImage";
    long time = System.currentTimeMillis();
    conf.set(YarnConfiguration.NM_LINUX_CONTAINER_EXECUTOR_PATH, executorPath);
    conf.set(YarnConfiguration.NM_LOCAL_DIRS, "/tmp/nm-local-dir" + time);
    conf.set(YarnConfiguration.NM_LOG_DIRS, "/tmp/userlogs" + time);
    conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME,
      yarnImage);
    conf.set(YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_EXEC_NAME,
      DOCKER_LAUNCH_COMMAND);
    dockerContainerExecutor = new DockerContainerExecutor();
    dirsHandler = new LocalDirsHandlerService();
    dirsHandler.init(conf);
    dockerContainerExecutor.setConf(conf);
    lfs = null;
    try {
      lfs = FileContext.getLocalFSFileContext();
      workDir = new Path("/tmp/temp-"+ System.currentTimeMillis());
      lfs.mkdir(workDir, FsPermission.getDirDefault(), true);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @After
  public void tearDown() {
    try {
      if (lfs != null) {
        lfs.delete(workDir, true);
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Test(expected = IllegalStateException.class)
  //Test that DockerContainerExecutor doesn't successfully init on a secure
  //cluster
  public void testContainerInitSecure() throws IOException {
    dockerContainerExecutor.getConf().set(
      CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, "kerberos");
    dockerContainerExecutor.init();
  }

  @Test(expected = IllegalArgumentException.class)
  //Test that when the image name is null, the container launch throws an
  //IllegalArgumentException
  public void testContainerLaunchNullImage() throws IOException {
    String appSubmitter = "nobody";
    String appId = "APP_ID";
    String containerId = "CONTAINER_ID";
    String testImage = "";

    Container container = mock(Container.class, RETURNS_DEEP_STUBS);
    ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS);
    ContainerLaunchContext context = mock(ContainerLaunchContext.class);

    HashMap<String, String> env = new HashMap<String,String>();
    when(container.getContainerId()).thenReturn(cId);
    when(container.getLaunchContext()).thenReturn(context);
    when(cId.getApplicationAttemptId().getApplicationId().toString())
      .thenReturn(appId);
    when(cId.toString()).thenReturn(containerId);

    when(context.getEnvironment()).thenReturn(env);
    env.put(
      YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
    dockerContainerExecutor.getConf().set(
      YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
    Path scriptPath = new Path("file:///bin/echo");
    Path tokensPath = new Path("file:///dev/null");

    Path pidFile = new Path(workDir, "pid.txt");

    dockerContainerExecutor.activateContainer(cId, pidFile);
    dockerContainerExecutor.launchContainer(new ContainerStartContext.Builder()
        .setContainer(container)
        .setNmPrivateContainerScriptPath(scriptPath)
        .setNmPrivateTokensPath(tokensPath)
        .setUser(appSubmitter)
        .setAppId(appId)
        .setContainerWorkDir(workDir)
        .setLocalDirs(dirsHandler.getLocalDirs())
        .setLogDirs(dirsHandler.getLogDirs())
        .build());
  }

  @Test(expected = IllegalArgumentException.class)
  //Test that when the image name is invalid, the container launch throws an
  //IllegalArgumentException
  public void testContainerLaunchInvalidImage() throws IOException {
    String appSubmitter = "nobody";
    String appId = "APP_ID";
    String containerId = "CONTAINER_ID";
    String testImage = "testrepo.com/test-image rm -rf $HADOOP_PREFIX/*";

    Container container = mock(Container.class, RETURNS_DEEP_STUBS);
    ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS);
    ContainerLaunchContext context = mock(ContainerLaunchContext.class);
    HashMap<String, String> env = new HashMap<String,String>();

    when(container.getContainerId()).thenReturn(cId);
    when(container.getLaunchContext()).thenReturn(context);
    when(cId.getApplicationAttemptId().getApplicationId().toString())
      .thenReturn(appId);
    when(cId.toString()).thenReturn(containerId);

    when(context.getEnvironment()).thenReturn(env);
    env.put(
      YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
    dockerContainerExecutor.getConf().set(
      YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
    Path scriptPath = new Path("file:///bin/echo");
    Path tokensPath = new Path("file:///dev/null");

    Path pidFile = new Path(workDir, "pid.txt");

    dockerContainerExecutor.activateContainer(cId, pidFile);
    dockerContainerExecutor.launchContainer(
        new ContainerStartContext.Builder()
            .setContainer(container)
            .setNmPrivateContainerScriptPath(scriptPath)
            .setNmPrivateTokensPath(tokensPath)
            .setUser(appSubmitter)
            .setAppId(appId)
            .setContainerWorkDir(workDir)
            .setLocalDirs(dirsHandler.getLocalDirs())
            .setLogDirs(dirsHandler.getLogDirs())
            .build());
  }

  @Test
  //Test that a container launch correctly wrote the session script with the
  //commands we expected
  public void testContainerLaunch() throws IOException {
    String appSubmitter = "nobody";
    String appId = "APP_ID";
    String containerId = "CONTAINER_ID";
    String testImage = "\"sequenceiq/hadoop-docker:2.4.1\"";

    Container container = mock(Container.class, RETURNS_DEEP_STUBS);
    ContainerId cId = mock(ContainerId.class, RETURNS_DEEP_STUBS);
    ContainerLaunchContext context = mock(ContainerLaunchContext.class);
    HashMap<String, String> env = new HashMap<String,String>();

    when(container.getContainerId()).thenReturn(cId);
    when(container.getLaunchContext()).thenReturn(context);
    when(cId.getApplicationAttemptId().getApplicationId().toString())
      .thenReturn(appId);
    when(cId.toString()).thenReturn(containerId);

    when(context.getEnvironment()).thenReturn(env);
    env.put(
      YarnConfiguration.NM_DOCKER_CONTAINER_EXECUTOR_IMAGE_NAME, testImage);
    Path scriptPath = new Path("file:///bin/echo");
    Path tokensPath = new Path("file:///dev/null");

    Path pidFile = new Path(workDir, "pid");

    dockerContainerExecutor.activateContainer(cId, pidFile);
    int ret = dockerContainerExecutor.launchContainer(
        new ContainerStartContext.Builder()
            .setContainer(container)
            .setNmPrivateContainerScriptPath(scriptPath)
            .setNmPrivateTokensPath(tokensPath)
            .setUser(appSubmitter)
            .setAppId(appId)
            .setContainerWorkDir(workDir)
            .setLocalDirs(dirsHandler.getLocalDirs())
            .setLogDirs(dirsHandler.getLogDirs())
            .build());
    assertEquals(0, ret);
    //get the script
    Path sessionScriptPath = new Path(workDir,
      Shell.appendScriptExtension(
        DockerContainerExecutor.DOCKER_CONTAINER_EXECUTOR_SESSION_SCRIPT));
    LineNumberReader lnr = new LineNumberReader(new FileReader(
      sessionScriptPath.toString()));
    boolean cmdFound = false;
    List<String> localDirs = dirsToMount(dirsHandler.getLocalDirs());
    List<String> logDirs = dirsToMount(dirsHandler.getLogDirs());
    List<String> workDirMount = dirsToMount(Collections.singletonList(
      workDir.toUri().getPath()));
    List<String> expectedCommands =  new ArrayList<String>(Arrays.asList(
      DOCKER_LAUNCH_COMMAND, "run", "--rm", "--net=host",  "--name",
      containerId));
    expectedCommands.addAll(localDirs);
    expectedCommands.addAll(logDirs);
    expectedCommands.addAll(workDirMount);
    String shellScript =  workDir + "/launch_container.sh";

    expectedCommands.addAll(Arrays.asList(testImage.replaceAll("['\"]", ""),
      "bash","\"" + shellScript + "\""));

    String expectedPidString =
      "echo `/bin/true inspect --format {{.State.Pid}} " + containerId+"` > "+
      pidFile.toString() + ".tmp";
    boolean pidSetterFound = false;
    while(lnr.ready()){
      String line = lnr.readLine();
      LOG.debug("line: " + line);
      if (line.startsWith(DOCKER_LAUNCH_COMMAND)){
        List<String> command = new ArrayList<String>();
        for( String s :line.split("\\s+")){
          command.add(s.trim());
        }

        assertEquals(expectedCommands, command);
        cmdFound = true;
      } else if (line.startsWith("echo")) {
        assertEquals(expectedPidString, line);
        pidSetterFound = true;
      }

    }
    assertTrue(cmdFound);
    assertTrue(pidSetterFound);
  }

  private List<String> dirsToMount(List<String> dirs) {
    List<String> localDirs = new ArrayList<String>();
    for(String dir: dirs){
      localDirs.add("-v");
      localDirs.add(dir + ":" + dir);
    }
    return localDirs;
  }
}
