| // 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"); |
| } |
| |
| Option<Error> error = validation::container::validateContainerId( |
| call.launch_nested_container().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 (!call.launch_nested_container().container_id().has_parent()) { |
| return Error("Expecting 'launch_nested_container.container_id.parent'" |
| " to be present"); |
| } |
| |
| if (call.launch_nested_container().has_command()) { |
| error = common::validation::validateCommandInfo( |
| call.launch_nested_container().command()); |
| if (error.isSome()) { |
| return Error("'launch_nested_container.command' is invalid" |
| ": " + error->message); |
| } |
| } |
| |
| if (call.launch_nested_container().has_container()) { |
| error = common::validation::validateContainerInfo( |
| call.launch_nested_container().container()); |
| if (error.isSome()) { |
| return Error("'launch_nested_container.container' is invalid" |
| ": " + error->message); |
| } |
| } |
| |
| 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"); |
| } |
| |
| Option<Error> error = validation::container::validateContainerId( |
| call.launch_nested_container_session().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 (!call.launch_nested_container_session().container_id().has_parent()) { |
| return Error( |
| "Expecting 'launch_nested_container_session.container_id.parent'" |
| " to be present"); |
| } |
| |
| if (call.launch_nested_container_session().has_command()) { |
| error = common::validation::validateCommandInfo( |
| call.launch_nested_container_session().command()); |
| if (error.isSome()) { |
| return Error("'launch_nested_container_session.command' is invalid" |
| ": " + error->message); |
| } |
| } |
| |
| if (call.launch_nested_container_session().has_container()) { |
| error = common::validation::validateContainerInfo( |
| call.launch_nested_container_session().container()); |
| if (error.isSome()) { |
| return Error("'launch_nested_container_session.container' is invalid" |
| ": " + error->message); |
| } |
| } |
| |
| 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"); |
| } |
| |
| Option<Error> error = validation::container::validateContainerId( |
| call.launch_container().container_id()); |
| |
| if (error.isSome()) { |
| return Error( |
| "'launch_container.container_id' is invalid: " + error->message); |
| } |
| |
| // Nested containers share resources with their parent so are |
| // not allowed to specify resources in this call. |
| if (call.launch_container().container_id().has_parent() && |
| call.launch_container().resources().size() != 0) { |
| return Error( |
| "Resources may not be specified when using " |
| "'launch_container' to launch nested containers"); |
| } |
| |
| // General resource validation first. |
| error = Resources::validate(call.launch_container().resources()); |
| if (error.isSome()) { |
| return Error("Invalid resources: " + error->message); |
| } |
| |
| error = common::validation::validateGpus( |
| call.launch_container().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. |
| foreach (Resource resource, call.launch_container().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 (call.launch_container().has_command()) { |
| error = common::validation::validateCommandInfo( |
| call.launch_container().command()); |
| if (error.isSome()) { |
| return Error( |
| "'launch_container.command' is invalid: " + error->message); |
| } |
| } |
| |
| if (call.launch_container().has_container()) { |
| error = common::validation::validateContainerInfo( |
| call.launch_container().container()); |
| if (error.isSome()) { |
| return Error( |
| "'launch_container.container' is invalid: " + error->message); |
| } |
| } |
| |
| 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::PRUNE_IMAGES: { |
| return None(); |
| } |
| } |
| |
| UNREACHABLE(); |
| } |
| |
| } // namespace call { |
| } // namespace agent { |
| } // namespace validation { |
| } // namespace slave { |
| } // namespace internal { |
| } // namespace mesos { |