blob: 0159b38f9b64c2c5feecc4e2e4fe070ce1f637e7 [file] [log] [blame]
// 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.
#include <sys/mount.h>
#include <glog/logging.h>
#include <process/id.hpp>
#include <stout/foreach.hpp>
#include <stout/option.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/strings.hpp>
#include <stout/os/exists.hpp>
#include <stout/os/mkdir.hpp>
#include <stout/os/realpath.hpp>
#include <stout/os/stat.hpp>
#include <stout/os/touch.hpp>
#include "common/protobuf_utils.hpp"
#include "common/validation.hpp"
#include "linux/fs.hpp"
#include "slave/containerizer/mesos/isolators/volume/host_path.hpp"
#include "slave/containerizer/mesos/isolators/volume/utils.hpp"
using std::string;
using process::Failure;
using process::Future;
using process::Owned;
using mesos::MountPropagation;
using mesos::slave::ContainerClass;
using mesos::slave::ContainerConfig;
using mesos::slave::ContainerLaunchInfo;
using mesos::slave::ContainerMountInfo;
using mesos::slave::Isolator;
using mesos::internal::slave::volume::PathValidator;
namespace mesos {
namespace internal {
namespace slave {
Try<Isolator*> VolumeHostPathIsolatorProcess::create(
const Flags& flags)
{
if (flags.launcher != "linux") {
return Error("'linux' launcher must be used");
}
if (!strings::contains(flags.isolation, "filesystem/linux")) {
return Error("'filesystem/linux' isolator must be used");
}
Owned<MesosIsolatorProcess> process(
flags.host_path_volume_force_creation.isSome()
? new VolumeHostPathIsolatorProcess(
flags,
PathValidator::parse(flags.host_path_volume_force_creation.get()))
: new VolumeHostPathIsolatorProcess(flags));
return new MesosIsolator(process);
}
VolumeHostPathIsolatorProcess::VolumeHostPathIsolatorProcess(
const Flags& _flags)
: ProcessBase(process::ID::generate("volume-host-path-isolator")),
flags(_flags),
pathValidator() {}
VolumeHostPathIsolatorProcess::VolumeHostPathIsolatorProcess(
const Flags& _flags,
const PathValidator& _pathValidator)
: ProcessBase(process::ID::generate("volume-host-path-isolator")),
flags(_flags),
pathValidator(_pathValidator) {}
VolumeHostPathIsolatorProcess::~VolumeHostPathIsolatorProcess() {}
bool VolumeHostPathIsolatorProcess::supportsNesting()
{
return true;
}
bool VolumeHostPathIsolatorProcess::supportsStandalone()
{
return true;
}
Future<Option<ContainerLaunchInfo>> VolumeHostPathIsolatorProcess::prepare(
const ContainerID& containerId,
const ContainerConfig& containerConfig)
{
if (!containerConfig.has_container_info()) {
return None();
}
const ContainerInfo& containerInfo = containerConfig.container_info();
if (containerInfo.type() != ContainerInfo::MESOS) {
return Failure("Only support MESOS containers");
}
ContainerLaunchInfo launchInfo;
foreach (const Volume& volume, containerInfo.volumes()) {
// NOTE: The validation here is for backwards compatibility. For
// example, if an old master (no validation code) is used to
// launch a task with a volume.
Option<Error> error = common::validation::validateVolume(volume);
if (error.isSome()) {
return Failure("Invalid volume: " + error->message);
}
Option<string> hostPath;
bool mountPropagationBidirectional = false;
// NOTE: This is the legacy way of specifying the Volume. The
// 'host_path' can be relative in legacy mode, representing
// SANDBOX_PATH volumes.
if (volume.has_host_path() &&
path::is_absolute(volume.host_path())) {
hostPath = volume.host_path();
}
if (volume.has_source() &&
volume.source().has_type() &&
volume.source().type() == Volume::Source::HOST_PATH) {
CHECK(volume.source().has_host_path());
const Volume::Source::HostPath& hostPathInfo =
volume.source().host_path();
if (!path::is_absolute(hostPathInfo.path())) {
return Failure(
"Path '" + hostPathInfo.path() + "' "
"in HOST_PATH volume is not absolute");
}
hostPath = hostPathInfo.path();
mountPropagationBidirectional =
hostPathInfo.has_mount_propagation() &&
hostPathInfo.mount_propagation().mode() ==
MountPropagation::BIDIRECTIONAL;
}
if (hostPath.isNone()) {
continue;
}
if (containerConfig.has_container_class() &&
containerConfig.container_class() == ContainerClass::DEBUG) {
return Failure("HOST_PATH volume is not supported for DEBUG containers");
}
if (!os::exists(hostPath.get())) {
if (pathValidator.isNone()) {
return Failure(
"Path '" + hostPath.get() + "' in HOST_PATH volume does not exist");
}
Try<string> normalizedPath = path::normalize(hostPath.get());
if (normalizedPath.isError()) {
return Failure(
"Failed to normalized the host path '" + hostPath.get() + "': " +
normalizedPath.error());
}
Try<Nothing> validate = pathValidator->validate(normalizedPath.get());
if (validate.isError()) {
return Failure(
"Path '" + hostPath.get() + "' in HOST_PATH volume does not exist "
"and is not whitelisted for creation: " + validate.error());
}
// Always assume the non-existing host path as a directory.
Try<Nothing> mkdir = os::mkdir(normalizedPath.get());
if (mkdir.isError()) {
return Failure(
"Failed to create the host path at '" + hostPath.get() + "': " +
mkdir.error());
}
}
// Determine the mount point for the host volume.
string mountPoint;
if (path::is_absolute(volume.container_path())) {
// TODO(jieyu): We need to check that the mount point resolves
// under 'rootfs' because a user can potentially use a container
// path like '/../../abc'.
if (containerConfig.has_rootfs()) {
mountPoint = path::join(
containerConfig.rootfs(),
volume.container_path());
if (os::stat::isdir(hostPath.get())) {
Try<Nothing> mkdir = os::mkdir(mountPoint);
if (mkdir.isError()) {
return Failure(
"Failed to create the mount point at "
"'" + mountPoint + "': " + mkdir.error());
}
} else {
// The file (regular file or device file) bind mount case.
Try<Nothing> mkdir = os::mkdir(Path(mountPoint).dirname());
if (mkdir.isError()) {
return Failure(
"Failed to create directory "
"'" + Path(mountPoint).dirname() + "' "
"for the mount point: " + mkdir.error());
}
Try<Nothing> touch = os::touch(mountPoint);
if (touch.isError()) {
return Failure(
"Failed to touch the mount point at "
"'" + mountPoint + "': " + touch.error());
}
}
} else {
mountPoint = volume.container_path();
// An absolute 'container_path' must already exist if the
// container rootfs is the same as the host. This is because
// we want to avoid creating mount points outside the work
// directory in the host filesystem.
if (!os::exists(mountPoint)) {
return Failure(
"Mount point '" + mountPoint + "' is an absolute path. "
"It must exist if the container shares the host filesystem");
}
}
} else {
// TODO(jieyu): We need to check that the mount point resolves
// under the sandbox because a user can potentially use a
// container path like '../../abc'.
// NOTE: If the container has its own rootfs, we cannot create
// the mount point in the mapped sandbox location in container's
// rootfs because the bind mount of the sandbox itself will hide
// the mount point. So we should always create the mount point
// in 'directory' first.
mountPoint = path::join(
containerConfig.directory(),
volume.container_path());
if (os::stat::isdir(hostPath.get())) {
Try<Nothing> mkdir = os::mkdir(mountPoint);
if (mkdir.isError()) {
return Failure(
"Failed to create the mount point at "
"'" + mountPoint + "': " + mkdir.error());
}
} else {
// The file (regular file or device file) bind mount case.
Try<Nothing> mkdir = os::mkdir(Path(mountPoint).dirname());
if (mkdir.isError()) {
return Failure(
"Failed to create the directory "
"'" + Path(mountPoint).dirname() + "' "
"for the mount point: " + mkdir.error());
}
Try<Nothing> touch = os::touch(mountPoint);
if (touch.isError()) {
return Failure(
"Failed to touch the mount point at "
"'" + mountPoint+ "': " + touch.error());
}
}
if (containerConfig.has_rootfs()) {
mountPoint = path::join(
containerConfig.rootfs(),
flags.sandbox_directory,
volume.container_path());
}
}
if (mountPropagationBidirectional) {
// First, find the mount entry that is the parent of the host
// volume source. If it is not a shared mount, return a failure.
Try<fs::MountInfoTable> table = fs::MountInfoTable::read();
if (table.isError()) {
return Failure("Failed to read mount table: " + table.error());
}
Try<fs::MountInfoTable::Entry> sourceMountEntry =
table->findByTarget(hostPath.get());
if (sourceMountEntry.isError()) {
return Failure(
"Cannot find the mount containing host path '" +
hostPath.get() + "': " + sourceMountEntry.error());
}
if (sourceMountEntry->shared().isNone()) {
return Failure(
"Cannot setup bidirectional mount propagation for host path '" +
hostPath.get() + "' because it is not under a shared mount");
}
LOG(INFO) << "Mark '" << sourceMountEntry->target
<< "' as shared for container " << containerId;
// This tells the launch helper to NOT mark the mount as slave
// (otherwise, the propagation won't work).
ContainerMountInfo* mount = launchInfo.add_mounts();
mount->set_target(sourceMountEntry->target);
mount->set_flags(MS_SHARED);
}
// NOTE: 'hostPath' and 'mountPoint' are equal only when the
// container does not define its own image and shares the host
// filesystem (otherwise, the mount point should be under
// container's rootfs, which won't be equal to 'hostPath'). As a
// result, no need for the bind mount because the 'hostPath' is
// already accessible in the container.
if (hostPath.get() != mountPoint) {
*launchInfo.add_mounts() = protobuf::slave::createContainerMount(
hostPath.get(),
mountPoint,
MS_BIND | MS_REC | (volume.mode() == Volume::RO ? MS_RDONLY : 0));
}
}
return launchInfo;
}
} // namespace slave {
} // namespace internal {
} // namespace mesos {