All Downloads are FREE. Search and download functionalities are using the official Maven repository.

container.Proot.scala Maven / Gradle / Ivy

The newest version!
package container

/*
 * Copyright (C) 2019 Romain Reuillon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see .
 */

import java.io.{ BufferedOutputStream, BufferedWriter, File, FileOutputStream, OutputStreamWriter, PrintStream, PrintWriter }
import java.nio.file.attribute.PosixFileAttributes

import container.ImageBuilder.checkImageFile
import container.OCI._
import container.Status._
import better.files._
import collection.JavaConverters._
import scala.sys.process._

object Proot {

  val scriptName = "launcher.sh"

  //    def loadImage(imagePath: String): Status = {
  //        // 1. We prepare the image by extracting it
  //        note("- preparing image")
  //        val directoryPath = prepareImage(imagePath) match {
  //            case (OK, Some(path))                 => path
  //            case (badStatus, _)             => return badStatus
  //        }
  //
  //        // 2. We explore the directory and start retrieving some info
  //        note("- analyzing image")
  //        val (manifestData, configData) = analyseImage(directoryPath) match {
  //            case (OK, Some(d1), Some(d2))   => (d1, d2)
  //            case (badStatus, _, _)          => return badStatus
  //        }
  //
  //        // 3. We squash the image by merging the image layers
  //        note("- squashing image")
  //        squashImage(directoryPath, manifestData.Layers) match {
  //            case OK                         =>
  //            case badStatus                  => return badStatus
  //        }
  //
  //        // 4. We prepare a bash script that will contain the required options for PRoot
  //        note("- preparing launcher script")
  //        generatePRootScript(directoryPath, Some(configData)) match {
  //            case OK                         =>
  //            case badStatus                  => return badStatus
  //        }
  //
  //        return OK
  //    }

  /**
   * Check that the image or directory exists, and extract it if it hasn't been already.
   * Return the status and the directory where the image has been extracted.
   */
  //    def prepareImage(imagePath: String): (Status, Option[String]) = {
  //        var status: Status = OK
  //        val file = new File(imagePath)
  //
  //        // first, we check that the path leads to somewhere
  //        if (!file.exists) return (INVALID_PATH.withArgument(imagePath), None)
  //
  //        // then, either we're given an archive, or a directory
  //        if (!file.isDirectory) {
  //            val (path, archiveName) = getPathAndFileName(imagePath)
  //            if(!isAnArchive(archiveName))
  //                return (INVALID_IMAGE.withArgument(archiveName + " is not a valid archive file."), None)
  //
  //            val directoryPath = getDirectoryPath(path, archiveName)
  //            status = createDirectory(directoryPath)
  //            if (status.isBad)
  //                return (status, None)
  //
  //            note("-- extracting archive")
  //            status = extractArchive(path + archiveName, directoryPath)
  //            if (status.isBad)
  //                return (status, None)
  //
  //            return (OK, Some(completeDirectoryPath(directoryPath)))
  //        }
  //        else {
  //            note("-- already extracted")
  //            return (OK, Some(completeDirectoryPath(imagePath)))
  //        }
  //    }

  /**
   * Retrieve metadata (layer ids, env variables, volumes, ports, commands)
   * from the manifest and configuration files.
   */
  //    def analyseImage(directoryPath: String): (Status, Option[ManifestData], Option[ConfigurationData]) = {
  //        val rootDirectory = new File(directoryPath)
  //
  //        assert(rootDirectory.exists)
  //
  //        note("-- extracting manifest data")
  //
  //        val manifest = getManifestFile(rootDirectory.list) match {
  //            case None       => return (INVALID_IMAGE_FORMAT.withArgument("No manifest.json file in the root directory."), None, None)
  //            case Some(path) => new File(directoryPath + path)
  //        }
  //
  //        val manifestLines = extractLines(manifest)
  //        val manifestData = harvestManifestData(manifestLines)
  //        note("manifestData: " + manifestData)
  //
  //        val config = new File(directoryPath + manifestData.Config)
  //        if (!config.exists) return (INVALID_IMAGE_FORMAT.withArgument("No configuration json file in the root directory."), None, None)
  //
  //        note("-- extracting configuration data")
  //
  //        val configLines = extractLines(config)
  //        val configData = harvestConfigData(configLines)
  //
  //        return (OK, Some(manifestData), Some(configData))
  //    }
  //

  /**
   * Merge the layers by extracting them in order in a same directory.
   * Also, delete the whiteout files.
   */
  //    def squashImage(directoryPath: String, layers: List[String]): Status = {
  //        val rootfsPath = directoryPath + rootfsName
  //        createDirectory(rootfsPath) match {
  //            case OK         =>
  //            case status     => return status
  //        }
  //
  //        layers.foreach{
  //            layerName => {
  //                note("-- extracting layer " + layerName)
  //                extractArchive(directoryPath + layerName, rootfsPath) match {
  //                    case OK         =>
  //                    case status     => return status
  //                }
  //             }
  //        }
  //
  //        note("-- removing whiteouts")
  //        removeWhiteouts(rootfsPath) match {
  //            case OK         =>
  //            case status     => return status
  //        }
  //
  //        return OK
  //    }

  val standardVarsFuncName = "prepareStandardVars"
  val envFuncName = "prepareEnv"
  val printCommandsFuncName = "printCommands"
  val infoVolumesFuncName = "infoVolumes"
  val infoPortsFuncName = "infoPorts"
  val runPRootFuncName = "runPRoot"
  val bashVarPrefix = "LOADER_"
  val workdirBashVAR = bashVarPrefix + "WORKDIR"
  val entryPointBashVar = bashVarPrefix + "ENTRYPOINT"
  val cmdBashVar = bashVarPrefix + "CMD"
  val launchScriptName = "launch.sh"

  object Script {
    //    def prepareVariables(args: Seq[String], functionName: String, write: String => Unit) = {
    //      write("function " + functionName + " {")
    //      for (arg <- args) write("\t" + addQuoteToRightSideOfEquality(arg))
    //      write("}\n")
    //    }

    def prepareEnvVariables(maybeArgs: Option[Seq[String]], functionName: String, write: String => Unit) = {
      write("function " + functionName + " {")
      maybeArgs match {
        case Some(args) => {
          for (arg <- args) write("\texport " + addQuoteToRightSideOfEquality(arg))
        }
        case _ => write("\t:")
      }
      write("}\n")
    }

    def prepareMapInfo(maybeMap: Option[Map[String, String]], title: String, functionName: String, write: String => Unit) = {
      write("function " + functionName + " {")
      write("\techo \"" + title + ":\"")
      maybeMap match {
        case Some(map) => for ((variable, _) <- map) write("\techo \"\t" + variable + "\"")
        case _ =>
      }
      write("}\n")
    }

    def assembleCommandParts(args: String*): String = {
      var command = ""
      for (arg <- args) command += arg + " "
      command
    }

    def assembleCommandParts(args: List[String]): String = assembleCommandParts(args: _*)

    def addQuoteToRightSideOfEquality(s: String) = {
      s.split('=') match {
        case Array(left, right) => left + "=\"" + right + "\""
        case _ => s
      }
    }
  }

  def generatePRootScript(
    scriptFile: java.io.File,
    proot: String,
    rootFS: String,
    workDirectory: Option[String],
    bind: Seq[(String, String)],
    containerEnvironmentVariables: Option[Seq[String]],
    environmentVariables: Seq[(String, String)],
    commandLines: Seq[String],
    noSeccomp: Boolean,
    kernel: Option[String]) = {

    scriptFile.getParentFile.mkdirs()

    val script = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(scriptFile)))
    try {
      def writeln(s: String) = script.write(s + "\n")

      writeln("#!/usr/bin/env bash\n")

      Script.prepareEnvVariables(containerEnvironmentVariables, envFuncName, writeln)

      val noSeccompVariable = if (noSeccomp) Seq("PROOT_NO_SECCOMP" -> "1") else Seq()

      preparePRootCommand(
        writeln,
        proot,
        rootFS = rootFS,
        workDirectory = workDirectory,
        bind = bind,
        environmentVariables = environmentVariables ++ noSeccompVariable,
        commandLines = commandLines,
        kernel = kernel)
      prepareCLI(writeln)
      script.flush()
    } finally script.close()

    scriptFile.setExecutable(true)
  }

  def prepareCLI(write: String => Unit) =
    write(s"""$runPRootFuncName $$1 "$$2" $${@:3}""".stripMargin)

  def preparePRootCommand(
    write: String => Unit,
    proot: String,
    rootFS: String,
    workDirectory: Option[String],
    bind: Seq[(String, String)],
    environmentVariables: Seq[(String, String)],
    commandLines: Seq[String],
    kernel: Option[String]) = {

    val workDirectoryArgs = workDirectory.filterNot(_.trim.isEmpty).map(w => s"-w $w").mkString(" ")
    val bindArgs = bind.map(b => s"""-b '${b._1}:${b._2}'""").mkString(" ")
    val kernelArg = kernel.map(k => s"-k $k").getOrElse("")

    write(
      s"""function $runPRootFuncName {
         |  PROOT=`which $proot`
         |  for i in $$(env | cut -d'=' -f1) ; do unset "$$i"; done
         |  ${environmentVariables.map { case (n, v) => s"export $n=$v" }.mkString("\n")}
         |  $envFuncName
         |  ${
        commandLines.map { commandLine =>
          Script.assembleCommandParts(
            "$PROOT", // calling PRoot
            "--kill-on-exit",
            "--netcoop",
            kernelArg,
            s"-r $rootFS", // setting guest rootfs
            workDirectoryArgs, // + workdirBashVAR, // setting working directory,
            bindArgs,
            commandLine // user inputs for the program
          )
        }.mkString("\n")
      }
         |}
         |""".stripMargin)
  }

  def execute(
    image: FlatImage,
    tmpDirectory: File,
    commands: Seq[String] = Vector.empty,
    proot: String = "proot",
    bind: Seq[(String, String)] = Vector.empty,
    workDirectory: Option[String] = None,
    environmentVariables: Seq[(String, String)] = Vector.empty,
    output: PrintStream = tool.outputLogger,
    error: PrintStream = tool.outputLogger,
    noSeccomp: Boolean = false,
    kernel: Option[String] = None): Int = {

    checkImageFile(image.file)

    val path = image.file.getAbsolutePath + "/"
    val rootFSPath = path + FlatImage.rootfsName

    val commandLines: Seq[String] = if (commands.isEmpty) Seq(image.command.mkString(" ")) else commands

    val script = (tmpDirectory.toScala / launchScriptName).toJava
    val workDirectoryValue = workDirectory.orElse(image.workDirectory)

    val additionalVariables = if (!environmentVariables.exists(_._1 == "HOME")) Seq("HOME" -> "/root") else Seq()

    generatePRootScript(
      script,
      proot = proot,
      rootFS = rootFSPath,
      workDirectory = workDirectoryValue,
      bind = bind,
      containerEnvironmentVariables = image.env,
      environmentVariables = environmentVariables ++ additionalVariables,
      commandLines = commandLines,
      noSeccomp = noSeccomp,
      kernel = kernel)

    try ProcessUtil.execute(Seq(script.getCanonicalPath), output, error)
    finally script.delete()
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy