// This test verifies that the slave task status label decorator can // add and remove labels from a TaskStatus during the status update // sequence. A TaskStatus with two labels ("foo":"bar" and // "bar":"baz") is sent from the executor. The labels get modified by // the slave hook to strip the "foo":"bar" pair and/ add a new // "baz":"qux" pair. TEST_F(HookTest, VerifySlaveTaskStatusDecorator) { Try<Owned<cluster::Master>> master = StartMaster(); ASSERT_SOME(master); MockExecutor exec(DEFAULT_EXECUTOR_ID); TestContainerizer containerizer(&exec); Owned<MasterDetector> detector = master.get()->createDetector(); Try<Owned<cluster::Slave>> slave = StartSlave(detector.get(), &containerizer); ASSERT_SOME(slave); MockScheduler sched; MesosSchedulerDriver driver( &sched, DEFAULT_FRAMEWORK_INFO, master.get()->pid, DEFAULT_CREDENTIAL); EXPECT_CALL(sched, registered(&driver, _, _)); Future<vector<Offer>> offers; EXPECT_CALL(sched, resourceOffers(&driver, _)) .WillOnce(FutureArg<1>(&offers)) .WillRepeatedly(Return()); // Ignore subsequent offers. driver.start(); AWAIT_READY(offers); ASSERT_EQ(1u, offers.get().size()); // Start a task. TaskInfo task = createTask(offers.get()[0], "", DEFAULT_EXECUTOR_ID); ExecutorDriver* execDriver; EXPECT_CALL(exec, registered(_, _, _, _)) .WillOnce(SaveArg<0>(&execDriver)); Future<TaskInfo> execTask; EXPECT_CALL(exec, launchTask(_, _)) .WillOnce(FutureArg<1>(&execTask)); Future<TaskStatus> status; EXPECT_CALL(sched, statusUpdate(&driver, _)) .WillOnce(FutureArg<1>(&status)); driver.launchTasks(offers.get()[0].id(), {task}); AWAIT_READY(execTask); // Now send TASK_RUNNING update with two labels. The first label // ("foo:bar") will be removed by the task status hook to ensure // that it can remove labels. The second label will be preserved // and forwarded to Master (and eventually to the framework). // The hook also adds a new label with the same key but a different // value ("bar:quz"). TaskStatus runningStatus; runningStatus.mutable_task_id()->MergeFrom(execTask.get().task_id()); runningStatus.set_state(TASK_RUNNING); // Add two labels to the TaskStatus Labels* labels = runningStatus.mutable_labels(); labels->add_labels()->CopyFrom(createLabel("foo", "bar")); labels->add_labels()->CopyFrom(createLabel("bar", "baz")); execDriver->sendStatusUpdate(runningStatus); AWAIT_READY(status); // The hook will hang an extra label off. const Labels& labels_ = status.get().labels(); EXPECT_EQ(2, labels_.labels_size()); // The test hook will prepend a new "baz":"qux" label. EXPECT_EQ("bar", labels_.labels(0).key()); EXPECT_EQ("qux", labels_.labels(0).value()); // And lastly, we only expect the "foo":"bar" pair to be stripped by // the module. The last pair should be the original "bar":"baz" // pair set by the test. EXPECT_EQ("bar", labels_.labels(1).key()); EXPECT_EQ("baz", labels_.labels(1).value()); // Now validate TaskInfo.container_status. We must have received a // container_status with one network_info set by the test hook module. EXPECT_TRUE(status.get().has_container_status()); EXPECT_EQ(1, status.get().container_status().network_infos().size()); const NetworkInfo networkInfo = status.get().container_status().network_infos(0); // The hook module sets up '4.3.2.1' as the IP address and 'public' as the // network isolation group. The `ip_address` field is deprecated, but the // hook module should continue to set it as well as the new `ip_addresses` // field for now. EXPECT_TRUE(networkInfo.has_ip_address()); EXPECT_EQ("4.3.2.1", networkInfo.ip_address()); EXPECT_EQ(1, networkInfo.ip_addresses().size()); EXPECT_TRUE(networkInfo.ip_addresses(0).has_ip_address()); EXPECT_EQ("4.3.2.1", networkInfo.ip_addresses(0).ip_address()); EXPECT_EQ(1, networkInfo.groups().size()); EXPECT_EQ("public", networkInfo.groups(0)); EXPECT_TRUE(networkInfo.has_labels()); EXPECT_EQ(1, networkInfo.labels().labels().size()); const Label networkInfoLabel = networkInfo.labels().labels(0); // Finally, the labels set inside NetworkInfo by the hook module. EXPECT_EQ("net_foo", networkInfoLabel.key()); EXPECT_EQ("net_bar", networkInfoLabel.value()); EXPECT_CALL(exec, shutdown(_)) .Times(AtMost(1)); driver.stop(); driver.join(); }
process::Future<Option<ContainerPrepareInfo>> NetworkIsolatorProcess::prepare( const ContainerID& containerId, const ExecutorInfo& executorInfo, const std::string& directory, const Option<std::string>& user) { LOG(INFO) << "NetworkIsolator::prepare for container: " << containerId; if (!executorInfo.has_container()) { LOG(INFO) << "NetworkIsolator::prepare Ignoring request as " << "executorInfo.container is missing for container: " << containerId; return None(); } if (executorInfo.container().network_infos().size() == 0) { LOG(INFO) << "NetworkIsolator::prepare Ignoring request as " << "executorInfo.container.network_infos is missing for " << "container: " << containerId; return None(); } if (executorInfo.container().network_infos().size() > 1) { return Failure( "NetworkIsolator:: multiple NetworkInfos are not supported."); } NetworkInfo networkInfo = executorInfo.container().network_infos(0); if (networkInfo.has_protocol()) { return Failure( "NetworkIsolator: NetworkInfo.protocol is deprecated and unsupported."); } if (networkInfo.has_ip_address()) { return Failure( "NetworkIsolator: NetworkInfo.ip_address is deprecated and" " unsupported."); } string uid = UUID::random().toString(); // Two IPAM commands: // 1) reserve for IPs the user has specifically asked for. // 2) auto-assign IPs. // Spin through all IPAddress messages once to get info for each command. // Then we'll issue each command if needed. IPAMReserveIPMessage reserveMessage; IPAMReserveIPMessage::Args* reserveArgs = reserveMessage.mutable_args(); // Counter of IPs to auto assign. int numIPv4 = 0; foreach (const NetworkInfo::IPAddress& ipAddress, networkInfo.ip_addresses()) { if (ipAddress.has_ip_address() && ipAddress.has_protocol()) { return Failure("NetworkIsolator: Cannot include both ip_address and " "protocol in a request."); } if (ipAddress.has_ip_address()) { // Store IP to attempt to reserve. reserveArgs->add_ipv4_addrs(ipAddress.ip_address()); } else if (ipAddress.has_protocol() && ipAddress.protocol() == NetworkInfo::IPv6){ return Failure("NetworkIsolator: IPv6 is not supported at this time."); } else { // Either protocol is IPv4, or not included (in which case we default to // IPv4 anyway). numIPv4++; } } if (!(reserveArgs->ipv4_addrs_size() + numIPv4)) { return Failure( "NetworkIsolator: Container requires at least one IP address."); } // All the IP addresses, both reserved and allocated. vector<string> allAddresses; // Reserve provided IPs first. if (reserveArgs->ipv4_addrs_size()) { reserveArgs->set_hostname(slaveInfo.hostname()); reserveArgs->set_uid(uid); reserveArgs->mutable_netgroups()->CopyFrom(networkInfo.groups()); LOG(INFO) << "Sending IP reserve command to IPAM"; Try<IPAMResponse> response = runCommand<IPAMReserveIPMessage, IPAMResponse>( ipamClientPath, reserveMessage); if (response.isError()) { return Failure("Error reserving IPs with IPAM: " + response.error()); } string addresses = ""; foreach (const string& addr, reserveArgs->ipv4_addrs()) { addresses = addresses + addr + " "; allAddresses.push_back(addr); } LOG(INFO) << "IP(s) " << addresses << "reserved with IPAM"; }