blob: 25e9fbd6a243d84db0f5ae0fe5e6c53c419bcadf [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 "slave/constants.hpp"
#include "slave/validation.hpp"
#include <string>
#include <mesos/resources.hpp>
#include <mesos/agent/agent.hpp>
#include <stout/stringify.hpp>
#include <stout/unreachable.hpp>
#include "common/resources_utils.hpp"
#include "common/validation.hpp"
using std::string;
namespace mesos {
namespace internal {
namespace slave {
namespace validation {
namespace container {
Option<Error> validateContainerId(const ContainerID& containerId)
{
const string& id = containerId.value();
// Check common Mesos ID rules.
Option<Error> error = common::validation::validateID(id);
if (error.isSome()) {
return Error(error->message);
}
// Check ContainerID specific rules.
//
// Valid the container id length
if (id.length() > MAX_CONTAINER_ID_LENGTH) {
return Error("'ContainerID.value' '" + id + "' exceeds the maximum"
" length (" + stringify(MAX_CONTAINER_ID_LENGTH) + ")");
}
// Periods are disallowed because our string representation of
// ContainerID uses periods: <uuid>.<child>.<grandchild>.
// For example: <uuid>.redis.backup
//
// Spaces are disallowed as they can render logs confusing and
// need escaping on terminals when dealing with paths.
auto invalidCharacter = [](char c) {
return c == '.' || c == ' ';
};
if (std::any_of(id.begin(), id.end(), invalidCharacter)) {
return Error("'ContainerID.value' '" + id + "'"
" contains invalid characters");
}
// TODO(bmahler): Print the invalid field nicely within the error
// (e.g. 'parent.parent.parent.value'). For now we only have one
// level of nesting so it's ok.
if (containerId.has_parent()) {
Option<Error> parentError = validateContainerId(containerId.parent());
if (parentError.isSome()) {
return Error("'ContainerID.parent' is invalid: " + parentError->message);
}
}
return None();
}
} // namespace container {
namespace agent {
namespace call {
Option<Error> validate(
const mesos::agent::Call& call,
const Option<string>& principal)
{
if (!call.IsInitialized()) {
return Error("Not initialized: " + call.InitializationErrorString());
}
if (!call.has_type()) {
return Error("Expecting 'type' to be present");
}
switch (call.type()) {
case mesos::agent::Call::UNKNOWN:
return None();
case mesos::agent::Call::GET_HEALTH:
return None();
case mesos::agent::Call::GET_FLAGS:
return None();
case mesos::agent::Call::GET_VERSION:
return None();
case mesos::agent::Call::GET_METRICS:
if (!call.has_get_metrics()) {
return Error("Expecting 'get_metrics' to be present");
}
return None();
case mesos::agent::Call::GET_LOGGING_LEVEL:
return None();
case mesos::agent::Call::SET_LOGGING_LEVEL:
if (!call.has_set_logging_level()) {
return Error("Expecting 'set_logging_level' to be present");
}
return None();
case mesos::agent::Call::LIST_FILES:
if (!call.has_list_files()) {
return Error("Expecting 'list_files' to be present");
}
return None();
case mesos::agent::Call::READ_FILE:
if (!call.has_read_file()) {
return Error("Expecting 'read_file' to be present");
}
return None();
case mesos::agent::Call::GET_STATE:
return None();
case mesos::agent::Call::GET_CONTAINERS:
return None();
case mesos::agent::Call::GET_FRAMEWORKS:
return None();
case mesos::agent::Call::GET_EXECUTORS:
return None();
case mesos::agent::Call::GET_OPERATIONS:
return None();
case mesos::agent::Call::GET_TASKS:
return None();
case mesos::agent::Call::GET_AGENT:
return None();
case mesos::agent::Call::GET_RESOURCE_PROVIDERS:
return None();
case mesos::agent::Call::LAUNCH_NESTED_CONTAINER: {
if (!call.has_launch_nested_container()) {
return Error("Expecting 'launch_nested_container' to be present");
}
const mesos::agent::Call::LaunchNestedContainer& launch =
call.launch_nested_container();
Option<Error> error = validation::container::validateContainerId(
launch.container_id());
if (error.isSome()) {
return Error("'launch_nested_container.container_id' is invalid"
": " + error->message);
}
// The parent `ContainerID` is required, so that we know
// which container to place it underneath.
if (!launch.container_id().has_parent()) {
return Error("Expecting 'launch_nested_container.container_id.parent'"
" to be present");
}
if (launch.has_command()) {
error = common::validation::validateCommandInfo(launch.command());
if (error.isSome()) {
return Error("'launch_nested_container.command' is invalid"
": " + error->message);
}
}
if (launch.has_container()) {
error = common::validation::validateContainerInfo(launch.container());
if (error.isSome()) {
return Error("'launch_nested_container.container' is invalid"
": " + error->message);
}
if (launch.container().has_linux_info() &&
launch.container().linux_info().has_share_cgroups() &&
!launch.container().linux_info().share_cgroups() &&
launch.container_id().has_parent() &&
launch.container_id().parent().has_parent()) {
return Error(
"'launch_nested_container' is invalid: containers nested at "
"the second level or greater cannot set 'share_cgroups' to "
"'false'");
}
}
return None();
}
case mesos::agent::Call::WAIT_NESTED_CONTAINER: {
if (!call.has_wait_nested_container()) {
return Error("Expecting 'wait_nested_container' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.wait_nested_container().container_id());
if (error.isSome()) {
return Error("'wait_nested_container.container_id' is invalid"
": " + error->message);
}
// Nested containers always have at least one parent.
if (!call.wait_nested_container().container_id().has_parent()) {
return Error("Expecting 'wait_nested_container.container_id.parent'"
" to be present");
}
return None();
}
case mesos::agent::Call::KILL_NESTED_CONTAINER: {
if (!call.has_kill_nested_container()) {
return Error("Expecting 'kill_nested_container' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.kill_nested_container().container_id());
if (error.isSome()) {
return Error("'kill_nested_container.container_id' is invalid"
": " + error->message);
}
// Nested containers always have at least one parent.
if (!call.kill_nested_container().container_id().has_parent()) {
return Error("Expecting 'kill_nested_container.container_id.parent'"
" to be present");
}
return None();
}
case mesos::agent::Call::REMOVE_NESTED_CONTAINER: {
if (!call.has_remove_nested_container()) {
return Error("Expecting 'remove_nested_container' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.remove_nested_container().container_id());
if (error.isSome()) {
return Error("'remove_nested_container.container_id' is invalid"
": " + error->message);
}
// Nested containers always have at least one parent.
if (!call.remove_nested_container().container_id().has_parent()) {
return Error("Expecting 'remove_nested_container.container_id.parent'"
" to be present");
}
return None();
}
case mesos::agent::Call::LAUNCH_NESTED_CONTAINER_SESSION: {
if (!call.has_launch_nested_container_session()) {
return Error(
"Expecting 'launch_nested_container_session' to be present");
}
const mesos::agent::Call::LaunchNestedContainerSession& launch =
call.launch_nested_container_session();
Option<Error> error = validation::container::validateContainerId(
launch.container_id());
if (error.isSome()) {
return Error("'launch_nested_container_session.container_id' is invalid"
": " + error->message);
}
// The parent `ContainerID` is required, so that we know
// which container to place it underneath.
if (!launch.container_id().has_parent()) {
return Error(
"Expecting 'launch_nested_container_session.container_id.parent'"
" to be present");
}
if (launch.has_command()) {
error = common::validation::validateCommandInfo(launch.command());
if (error.isSome()) {
return Error("'launch_nested_container_session.command' is invalid"
": " + error->message);
}
}
if (launch.has_container()) {
error = common::validation::validateContainerInfo(launch.container());
if (error.isSome()) {
return Error("'launch_nested_container_session.container' is invalid"
": " + error->message);
}
if (launch.container().has_linux_info() &&
!launch.container().linux_info().share_cgroups()) {
return Error(
"'launch_nested_container_session.container.linux_info' is "
"invalid: 'share_cgroups' cannot be set to 'false' for nested "
"container sessions");
}
}
return None();
}
case mesos::agent::Call::ATTACH_CONTAINER_INPUT: {
if (!call.has_attach_container_input()) {
return Error("Expecting 'attach_container_input' to be present");
}
if (!call.attach_container_input().has_type()) {
return Error("Expecting 'attach_container_input.type' to be present");
}
switch (call.attach_container_input().type()) {
case mesos::agent::Call::AttachContainerInput::UNKNOWN:
return Error("'attach_container_input.type' is unknown");
case mesos::agent::Call::AttachContainerInput::CONTAINER_ID: {
Option<Error> error = validation::container::validateContainerId(
call.attach_container_input().container_id());
if (error.isSome()) {
return Error("'attach_container_input.container_id' is invalid"
": " + error->message);
}
}
case mesos::agent::Call::AttachContainerInput::PROCESS_IO:
return None();
}
UNREACHABLE();
}
case mesos::agent::Call::ATTACH_CONTAINER_OUTPUT: {
if (!call.has_attach_container_output()) {
return Error("Expecting 'attach_container_output' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.attach_container_output().container_id());
if (error.isSome()) {
return Error("'attach_container_output.container_id' is invalid"
": " + error->message);
}
return None();
}
case mesos::agent::Call::LAUNCH_CONTAINER: {
if (!call.has_launch_container()) {
return Error("Expecting 'launch_container' to be present");
}
const mesos::agent::Call::LaunchContainer& launch =
call.launch_container();
Option<Error> error = validation::container::validateContainerId(
launch.container_id());
if (error.isSome()) {
return Error(
"'launch_container.container_id' is invalid: " + error->message);
}
// General resource validation first.
error = Resources::validate(launch.resources());
if (error.isSome()) {
return Error("Invalid resources: " + error->message);
}
error = common::validation::validateGpus(launch.resources());
if (error.isSome()) {
return Error("Invalid GPU resources: " + error->message);
}
// Because standalone containers are launched outside of the master's
// offer cycle, some resource types or fields may not be specified.
if (!launch.container_id().has_parent()) {
foreach (Resource resource, launch.resources()) {
// Upgrade the resources (in place) to simplify validation.
upgradeResource(&resource);
// Standalone containers may only use unreserved resources.
// There is no accounting in the master for resources consumed
// by standalone containers, so allowing reserved resources would
// only increase code complexity with no change in behavior.
if (Resources::isReserved(resource)) {
return Error("'launch_container.resources' must be unreserved");
}
// NOTE: The master normally requires all volumes be persistent,
// and that all persistent volumes belong to a role. Standalone
// containers therefore cannot use persistent volumes.
if (Resources::isPersistentVolume(resource)) {
return Error(
"'launch_container.resources' may not use persistent volumes");
}
// Standalone containers are expected to occupy resources *not*
// advertised by the agent and hence do not need to worry about
// being preempted or throttled.
if (Resources::isRevocable(resource)) {
return Error("'launch_container.resources' must be non-revocable");
}
}
}
if (launch.has_command()) {
error = common::validation::validateCommandInfo(launch.command());
if (error.isSome()) {
return Error(
"'launch_container.command' is invalid: " + error->message);
}
}
if (launch.has_container()) {
error = common::validation::validateContainerInfo(launch.container());
if (error.isSome()) {
return Error(
"'launch_container.container' is invalid: " + error->message);
}
}
bool shareCgroups =
(launch.has_container() &&
launch.container().has_linux_info() &&
launch.container().linux_info().has_share_cgroups()) ?
launch.container().linux_info().share_cgroups() :
true;
bool twiceNested =
(launch.container_id().has_parent() &&
launch.container_id().parent().has_parent());
if (twiceNested && !launch.resources().empty()) {
return Error(
"'launch_container' is invalid: containers nested at the "
"second level or greater cannot specify resources");
}
if (twiceNested && !launch.limits().empty()) {
return Error(
"'launch_container' is invalid: containers nested at the "
"second level or greater cannot specify resource limits");
}
if (twiceNested && !shareCgroups) {
return Error(
"'launch_container' is invalid: containers nested at the "
"second level or greater cannot set 'share_cgroups' to "
"'false'");
}
if (!launch.container_id().has_parent() &&
launch.container().linux_info().has_share_cgroups() &&
shareCgroups) {
return Error(
"'launch_container' is invalid: containers without a parent "
"cannot set 'share_cgroups' to 'true'");
}
return None();
}
case mesos::agent::Call::WAIT_CONTAINER: {
if (!call.has_wait_container()) {
return Error("Expecting 'wait_container' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.wait_container().container_id());
if (error.isSome()) {
return Error("'wait_container.container_id' is invalid"
": " + error->message);
}
return None();
}
case mesos::agent::Call::KILL_CONTAINER: {
if (!call.has_kill_container()) {
return Error("Expecting 'kill_container' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.kill_container().container_id());
if (error.isSome()) {
return Error("'kill_container.container_id' is invalid"
": " + error->message);
}
return None();
}
case mesos::agent::Call::REMOVE_CONTAINER: {
if (!call.has_remove_container()) {
return Error("Expecting 'remove_container' to be present");
}
Option<Error> error = validation::container::validateContainerId(
call.remove_container().container_id());
if (error.isSome()) {
return Error("'remove_container.container_id' is invalid"
": " + error->message);
}
return None();
}
case mesos::agent::Call::ADD_RESOURCE_PROVIDER_CONFIG: {
if (!call.has_add_resource_provider_config()) {
return Error(
"Expecting 'add_resource_provider_config' to be present");
}
if (call.add_resource_provider_config().info().has_id()) {
return Error(
"Expecting 'add_resource_provider_config.info.id' to be unset");
}
return None();
}
case mesos::agent::Call::UPDATE_RESOURCE_PROVIDER_CONFIG: {
if (!call.has_update_resource_provider_config()) {
return Error(
"Expecting 'update_resource_provider_config' to be present");
}
if (call.update_resource_provider_config().info().has_id()) {
return Error(
"Expecting 'update_resource_provider_config.info.id' to be unset");
}
return None();
}
case mesos::agent::Call::REMOVE_RESOURCE_PROVIDER_CONFIG: {
if (!call.has_remove_resource_provider_config()) {
return Error(
"Expecting 'remove_resource_provider_config' to be present");
}
return None();
}
case mesos::agent::Call::MARK_RESOURCE_PROVIDER_GONE: {
if (!call.has_mark_resource_provider_gone()) {
return Error("Expecting 'mark_resource_provider_gone' to be present");
}
return None();
}
case mesos::agent::Call::PRUNE_IMAGES: {
return None();
}
}
UNREACHABLE();
}
} // namespace call {
} // namespace agent {
} // namespace validation {
} // namespace slave {
} // namespace internal {
} // namespace mesos {