diff options
author | Martin Polden <martin.polden@gmail.com> | 2017-01-23 12:56:18 +0100 |
---|---|---|
committer | Martin Polden <martin.polden@gmail.com> | 2017-01-23 15:31:34 +0100 |
commit | ce461f93e3de16379df0a1b2e1d6f62ca8a8d25d (patch) | |
tree | c9ff6773f97184c4c4eb2db4c0f0745f5b514dd1 /node-admin | |
parent | c40e51eca9838bbc75393333d4a93f5b942b1d65 (diff) |
Add Docker ACL maintainer
Diffstat (limited to 'node-admin')
16 files changed, 483 insertions, 31 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java new file mode 100644 index 00000000000..545dcd66666 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/ContainerAclSpec.java @@ -0,0 +1,31 @@ +package com.yahoo.vespa.hosted.node.admin; + +/** + * An ACL specification for a container. + * + * @author mpolden + */ +public class ContainerAclSpec { + + private final String hostname; + private final String ipAddress; + private final String trustedBy; + + public ContainerAclSpec(String hostname, String ipAddress, String trustedBy) { + this.hostname = hostname; + this.ipAddress = ipAddress; + this.trustedBy = trustedBy; + } + + public String hostname() { + return hostname; + } + + public String ipAddress() { + return ipAddress; + } + + public String trustedBy() { + return trustedBy; + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java index 5de85d3b506..52b5bede912 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java @@ -27,6 +27,8 @@ public interface DockerOperations { void executeCommandInContainer(ContainerName containerName, String[] command); + void executeCommandInNetworkNamespace(ContainerName containerName, String[] command); + void resumeNode(ContainerName containerName); void restartServicesOnNode(ContainerName containerName); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java index 28d0a5bafa9..73d66d685a9 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java @@ -18,6 +18,7 @@ import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.util.Environment; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; +import java.io.IOException; import java.io.InputStreamReader; import java.net.Inet6Address; import java.net.InetAddress; @@ -29,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -76,12 +78,18 @@ public class DockerOperationsImpl implements DockerOperations { private final Docker docker; private final Environment environment; + private final Consumer<List<String>> commandExecutor; private GaugeWrapper numberOfRunningContainersGauge; public DockerOperationsImpl(Docker docker, Environment environment, MetricReceiverWrapper metricReceiver) { + this(docker, environment, metricReceiver, DockerOperationsImpl::runCommand); + } + + DockerOperationsImpl(Docker docker, Environment environment, MetricReceiverWrapper metricReceiver, Consumer<List<String>> commandExecutor) { this.docker = docker; this.environment = environment; setMetrics(metricReceiver); + this.commandExecutor = commandExecutor; } @Override @@ -262,7 +270,7 @@ public class DockerOperationsImpl implements DockerOperations { for (int retry = 0; retry < 30; ++retry) { try { - runCommand(command); + commandExecutor.accept(command); logger.info("Done setting up network"); return; } catch (Exception e) { @@ -278,18 +286,6 @@ public class DockerOperationsImpl implements DockerOperations { } } - private void runCommand(final List<String> command) throws Exception { - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - Process process = builder.start(); - - String output = CharStreams.toString(new InputStreamReader(process.getInputStream())); - int resultCode = process.waitFor(); - if (resultCode != 0) { - throw new Exception("Command " + Joiner.on(' ').join(command) + " failed: " + output); - } - } - @Override public void scheduleDownloadOfImage(final ContainerNodeSpec nodeSpec, Runnable callback) { PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperationsImpl.class, nodeSpec.containerName); @@ -331,6 +327,31 @@ public class DockerOperationsImpl implements DockerOperations { } @Override + public void executeCommandInNetworkNamespace(ContainerName containerName, String[] command) { + final PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperationsImpl.class, containerName); + final Docker.ContainerInfo containerInfo = docker.inspectContainer(containerName) + .orElseThrow(() -> new RuntimeException("Container " + containerName + " does not exist")); + final Integer containerPid = containerInfo.getPid() + .orElseThrow(() -> new RuntimeException("Container " + containerName + " isn't running (pid not found)")); + + final List<String> wrappedCommand = new LinkedList<>(); + wrappedCommand.add("sudo"); + wrappedCommand.add("-n"); // Run non-interactively and fail if a password is required + wrappedCommand.add("nsenter"); + wrappedCommand.add(String.format("--net=/host/proc/%d/ns/net", containerPid)); + wrappedCommand.add("--"); + wrappedCommand.addAll(Arrays.asList(command)); + + try { + commandExecutor.accept(wrappedCommand); + } catch (Exception e) { + logger.error(String.format("Failed to execute %s in network namespace for %s (PID = %d)", + Arrays.toString(command), containerName.asString(), containerPid)); + throw new RuntimeException(e); + } + } + + @Override public void resumeNode(ContainerName containerName) { executeCommandInContainer(containerName, RESUME_NODE_COMMAND); } @@ -370,4 +391,19 @@ public class DockerOperationsImpl implements DockerOperations { // Some containers could already be running, count them and initialize to that value numberOfRunningContainersGauge.sample(getAllManagedContainers().size()); } + + private static void runCommand(final List<String> command) { + ProcessBuilder builder = new ProcessBuilder(command); + builder.redirectErrorStream(true); + try { + Process process = builder.start(); + String output = CharStreams.toString(new InputStreamReader(process.getInputStream())); + int resultCode = process.waitFor(); + if (resultCode != 0) { + throw new RuntimeException("Command " + Joiner.on(' ').join(command) + " failed: " + output); + } + } catch (IOException|InterruptedException e) { + throw new RuntimeException(e); + } + } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java new file mode 100644 index 00000000000..4deefff44c5 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainer.java @@ -0,0 +1,111 @@ +package com.yahoo.vespa.hosted.node.admin.maintenance; + +import com.google.common.net.InetAddresses; +import com.yahoo.net.HostName; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; +import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.util.IpTables; +import com.yahoo.vespa.hosted.node.admin.util.IpTables.Policy; +import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; + +import java.net.Inet6Address; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * The responsibility of this class is to configure ACLs for all running containers. The ACLs are fetched from the Node + * repository. Based on those ACLs, iptables commands are created and then executed in each of the containers network + * namespace. + * + * If a rule cannot be configured (e.g. iptables process execution fails), a rollback is attempted by setting the + * default policy to ACCEPT which will allow any traffic. The configuration will be retried the next time the + * maintainer runs. + * + * The ACL maintainer does not handle IPv4 addresses and is thus only intended to configure ACLs for IPv6-only + * containers (e.g. any container, except node-admin). + * + * @author mpolden + */ +public class AclMaintainer implements Runnable { + + private static final PrefixLogger log = PrefixLogger.getNodeAdminLogger(AclMaintainer.class); + + private final DockerOperations dockerOperations; + private final NodeRepository nodeRepository; + private final Supplier<String> nodeAdminHostnameSupplier; + + public AclMaintainer(DockerOperations dockerOperations, NodeRepository nodeRepository) { + this(dockerOperations, nodeRepository, HostName::getLocalhost); + } + + AclMaintainer(DockerOperations dockerOperations, NodeRepository nodeRepository, + Supplier<String> nodeAdminHostnameSupplier) { + this.dockerOperations = dockerOperations; + this.nodeRepository = nodeRepository; + this.nodeAdminHostnameSupplier = nodeAdminHostnameSupplier; + } + + private void applyAcl(ContainerName containerName, List<ContainerAclSpec> aclSpecs) { + try { + dockerOperations.executeCommandInNetworkNamespace(containerName, IpTables.flushChain()); + aclSpecs.stream() + .map(ContainerAclSpec::ipAddress) + .filter(ipAddress -> { + final boolean isIpv6 = isIpv6(ipAddress); + if (!isIpv6) { + log.warning("Skipping unexpected IPv4 address in ACL configuration: " + ipAddress); + } + return isIpv6; + }) + .forEach(ipAddress -> dockerOperations.executeCommandInNetworkNamespace(containerName, + IpTables.allowFromAddress(ipAddress))); + dockerOperations.executeCommandInNetworkNamespace(containerName, IpTables.chainPolicy(Policy.DROP)); + } catch (Exception e) { + log.error("Exception occurred while configuring ACLs, attempting rollback", e); + try { + dockerOperations.executeCommandInNetworkNamespace(containerName, IpTables.chainPolicy(Policy.ACCEPT)); + } catch (Exception ne) { + log.error("Rollback failed, giving up", ne); + } + } + } + + private void configureAcls() { + final List<ContainerAclSpec> aclSpecs = nodeRepository.getContainerAclSpecs(nodeAdminHostnameSupplier.get()); + final Map<String, List<ContainerAclSpec>> aclSpecsGroupedByHostname = aclSpecs.stream() + .collect(Collectors.groupingBy(ContainerAclSpec::trustedBy)); + + for (Map.Entry<String, List<ContainerAclSpec>> entry : aclSpecsGroupedByHostname.entrySet()) { + final String hostname = entry.getKey(); + final Optional<Container> container = dockerOperations.getContainer(hostname); + if (!container.isPresent()) { + log.warning("Got ACL for hostname " + hostname + ", but no container with that hostname exist"); + continue; + } + if (!container.get().isRunning) { + log.info("Skipping ACL configuration for stopped container " + container.get().hostname); + continue; + } + applyAcl(container.get().name, entry.getValue()); + } + } + + @Override + public void run() { + try { + configureAcls(); + } catch (Throwable t) { + log.error("Failed to configure ACLs", t); + } + } + + private static boolean isIpv6(String ipAddress) { + return InetAddresses.forString(ipAddress) instanceof Inet6Address; + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java index 3356d36f9b7..391368642ee 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImpl.java @@ -3,14 +3,15 @@ package com.yahoo.vespa.hosted.node.admin.nodeadmin; import com.yahoo.collections.Pair; import com.yahoo.net.HostName; +import com.yahoo.vespa.hosted.dockerapi.Container; import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.metrics.CounterWrapper; import com.yahoo.vespa.hosted.dockerapi.metrics.Dimensions; import com.yahoo.vespa.hosted.dockerapi.metrics.GaugeWrapper; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; -import com.yahoo.vespa.hosted.dockerapi.Container; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.maintenance.AclMaintainer; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; @@ -31,8 +32,6 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - /** * Administers a host (for now only docker hosts) and its nodes (docker containers nodes). * @@ -40,11 +39,12 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; */ public class NodeAdminImpl implements NodeAdmin { private static final PrefixLogger logger = PrefixLogger.getNodeAdminLogger(NodeAdmin.class); - private final ScheduledExecutorService metricsFetcherScheduler = Executors.newScheduledThreadPool(1); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final DockerOperations dockerOperations; private final Function<String, NodeAgent> nodeAgentFactory; private final Optional<StorageMaintainer> storageMaintainer; + private final Optional<AclMaintainer> aclMaintainer; private AtomicBoolean frozen = new AtomicBoolean(false); private final Map<String, NodeAgent> nodeAgents = new HashMap<>(); @@ -56,11 +56,12 @@ public class NodeAdminImpl implements NodeAdmin { private CounterWrapper numberOfUnhandledExceptionsInNodeAgent; public NodeAdminImpl(final DockerOperations dockerOperations, final Function<String, NodeAgent> nodeAgentFactory, - final Optional<StorageMaintainer> storageMaintainer, int nodeAgentScanIntervalMillis, - final MetricReceiverWrapper metricReceiver) { + final Optional<StorageMaintainer> storageMaintainer, final int nodeAgentScanIntervalMillis, + final MetricReceiverWrapper metricReceiver, final Optional<AclMaintainer> aclMaintainer) { this.dockerOperations = dockerOperations; this.nodeAgentFactory = nodeAgentFactory; this.storageMaintainer = storageMaintainer; + this.aclMaintainer = aclMaintainer; this.nodeAgentScanIntervalMillis = nodeAgentScanIntervalMillis; Dimensions dimensions = new Dimensions.Builder() @@ -71,13 +72,15 @@ public class NodeAdminImpl implements NodeAdmin { this.numberOfContainersInLoadImageState = metricReceiver.declareGauge(dimensions, "nodes.image.loading"); this.numberOfUnhandledExceptionsInNodeAgent = metricReceiver.declareCounter(dimensions, "nodes.unhandled_exceptions"); - metricsFetcherScheduler.scheduleWithFixedDelay(() -> { + scheduler.scheduleWithFixedDelay(() -> { try { nodeAgents.values().forEach(NodeAgent::updateContainerNodeMetrics); } catch (Throwable e) { logger.warning("Metric fetcher scheduler failed", e); } - }, 0, 30000, MILLISECONDS); + }, 0, 30000, TimeUnit.MILLISECONDS); + + this.aclMaintainer.ifPresent(maintainer -> scheduler.scheduleAtFixedRate(maintainer, 30, 60, TimeUnit.SECONDS)); } public void refreshContainersToRun(final List<ContainerNodeSpec> containersToRun) { @@ -174,9 +177,9 @@ public class NodeAdminImpl implements NodeAdmin { @Override public void shutdown() { - metricsFetcherScheduler.shutdown(); + scheduler.shutdown(); try { - if (! metricsFetcherScheduler.awaitTermination(30, TimeUnit.SECONDS)) { + if (! scheduler.awaitTermination(30, TimeUnit.SECONDS)) { throw new RuntimeException("Did not manage to shutdown node-agent metrics update metricsFetcherScheduler."); } } catch (InterruptedException e) { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java index c6613610150..94a7d7d35b8 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java @@ -1,6 +1,7 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.noderepository; +import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; @@ -16,6 +17,8 @@ public interface NodeRepository { Optional<ContainerNodeSpec> getContainerNodeSpec(String hostName); + List<ContainerAclSpec> getContainerAclSpecs(String hostName); + void updateNodeAttributes(String hostName, NodeAttributes nodeAttributes); void markAsReady(String hostName); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java index d6272c40674..db14c3b62d8 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java @@ -1,10 +1,12 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.noderepository; +import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; +import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetAclResponse; import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetNodesResponse; import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.NodeReadyResponse; import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.UpdateNodeAttributesRequestBody; @@ -15,10 +17,12 @@ import com.yahoo.vespa.hosted.provision.Node; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * @author stiankri, dybis @@ -79,6 +83,19 @@ public class NodeRepositoryImpl implements NodeRepository { } } + @Override + public List<ContainerAclSpec> getContainerAclSpecs(String hostName) { + try { + final String path = String.format("/nodes/v2/acl/%s?children=true", hostName); + final GetAclResponse response = requestExecutor.get(path, port, GetAclResponse.class); + return response.trustedNodes.stream() + .map(node -> new ContainerAclSpec(node.hostname, node.ipAddress, node.trustedBy)) + .collect(Collectors.toList()); + } catch (ConfigServerHttpRequestExecutor.NotFoundException e) { + return Collections.emptyList(); + } + } + private static ContainerNodeSpec createContainerNodeSpec(GetNodesResponse.Node node) throws IllegalArgumentException, NullPointerException { Objects.requireNonNull(node.nodeState, "Unknown node state"); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java new file mode 100644 index 00000000000..8798323cf4d --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java @@ -0,0 +1,45 @@ +package com.yahoo.vespa.hosted.node.admin.noderepository.bindings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Collections; +import java.util.List; + +/** + * This class represents a response from the /nodes/v2/acl/ API. + * + * @author mpolden + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetAclResponse { + + @JsonProperty("trustedNodes") + public final List<Node> trustedNodes; + + @JsonCreator + public GetAclResponse(@JsonProperty("trustedNodes") List<Node> trustedNodes) { + this.trustedNodes = trustedNodes == null ? Collections.emptyList() : trustedNodes; + } + + public static class Node { + + @JsonProperty("hostname") + public final String hostname; + + @JsonProperty("ipAddress") + public final String ipAddress; + + @JsonProperty("trustedBy") + public final String trustedBy; + + @JsonCreator + public Node(@JsonProperty("hostname") String hostname, @JsonProperty("ipAddress") String ipAddress, + @JsonProperty("trustedBy") String trustedBy) { + this.hostname = hostname; + this.ipAddress = ipAddress; + this.trustedBy = trustedBy; + } + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java index 5dffc1852d2..58095c14a9b 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/provider/ComponentsProviderImpl.java @@ -8,6 +8,7 @@ import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.maintenance.AclMaintainer; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdmin; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; @@ -60,8 +61,10 @@ public class ComponentsProviderImpl implements ComponentsProvider { final Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl(hostName, nodeRepository, orchestrator, dockerOperations, storageMaintainer, metricReceiver, environment); + final AclMaintainer aclMaintainer = new AclMaintainer(dockerOperations, nodeRepository); + final NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, storageMaintainer, - NODE_AGENT_SCAN_INTERVAL_MILLIS, metricReceiver); + NODE_AGENT_SCAN_INTERVAL_MILLIS, metricReceiver, Optional.of(aclMaintainer)); nodeAdminStateUpdater = new NodeAdminStateUpdater(nodeRepository, nodeAdmin, INITIAL_SCHEDULER_DELAY_MILLIS, NODE_ADMIN_STATE_INTERVAL_MILLIS, orchestrator, baseHostName); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java new file mode 100644 index 00000000000..eb154c098bc --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/IpTables.java @@ -0,0 +1,34 @@ +package com.yahoo.vespa.hosted.node.admin.util; + +/** + * Utility class for creating iptables commands + * + * @author mpolden + */ +public class IpTables { + + private static final String COMMAND = "ip6tables"; + + public static String[] allowFromAddress(String ipAddress) { + return new String[]{COMMAND, "-A", "INPUT", "-s", ipAddress, "-j", "ACCEPT"}; + } + + public static String[] chainPolicy(Policy policy) { + return new String[]{COMMAND, "-P", "INPUT", policy.target}; + } + + public static String[] flushChain() { + return new String[]{COMMAND, "-F", "INPUT"}; + } + + public enum Policy { + DROP("DROP"), + ACCEPT("ACCEPT"); + + private final String target; + + Policy(String target) { + this.target = target; + } + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java index b3b804ec7ad..2b68c869008 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImplTest.java @@ -11,11 +11,17 @@ import org.hamcrest.CoreMatchers; import org.junit.Test; import org.mockito.InOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyVararg; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -123,4 +129,31 @@ public class DockerOperationsImplTest { public void vespaVersionIsNotParsedFromUnexpectedContent() { assertThat(DockerOperationsImpl.parseVespaVersion("No such command 'vespanodectl'"), CoreMatchers.is(Optional.empty())); } -}
\ No newline at end of file + + @Test + public void runsCommandInNetworkNamespace() { + ContainerName container = makeContainer("container-42", 42); + List<String> capturedArgs = new ArrayList<>(); + DockerOperationsImpl dockerOperations = new DockerOperationsImpl(docker, environment, + new MetricReceiverWrapper(MetricReceiver.nullImplementation), capturedArgs::addAll); + + dockerOperations.executeCommandInNetworkNamespace(container, new String[]{"iptables", "-nvL"}); + + assertEquals(Arrays.asList( + "sudo", + "-n", + "nsenter", + "--net=/host/proc/42/ns/net", + "--", + "iptables", + "-nvL"), capturedArgs); + } + + private ContainerName makeContainer(String hostname, int pid) { + ContainerName containerName = new ContainerName(hostname); + Docker.ContainerInfo containerInfo = mock(Docker.ContainerInfo.class); + when(containerInfo.getPid()).thenReturn(Optional.of(pid)); + when(docker.inspectContainer(eq(containerName))).thenReturn(Optional.of(containerInfo)); + return containerName; + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java index dff653ab94a..664d375ec09 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/ComponentsProviderWithMocks.java @@ -34,7 +34,7 @@ public class ComponentsProviderWithMocks implements ComponentsProvider { private final Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl(hostName, nodeRepositoryMock, orchestratorMock, dockerOperations, Optional.empty(), mr, environment); - private NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.empty(), 100, mr); + private NodeAdmin nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.empty(), 100, mr, Optional.empty()); @Override diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java index 01551b28c02..0912ccff814 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/DockerTester.java @@ -59,7 +59,7 @@ public class DockerTester implements AutoCloseable { final DockerOperations dockerOperations = new DockerOperationsImpl(dockerMock, environment, mr); Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl(hostName, nodeRepositoryMock, orchestratorMock, dockerOperations, Optional.of(storageMaintainer), mr, environment); - nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.of(storageMaintainer), 100, mr); + nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.of(storageMaintainer), 100, mr, Optional.empty()); updater = new NodeAdminStateUpdater(nodeRepositoryMock, nodeAdmin, 1, 1, orchestratorMock, "basehostname"); } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java index 77f2ffe518a..75faf3bd484 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java @@ -1,6 +1,7 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.integrationTests; +import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; @@ -8,7 +9,10 @@ import com.yahoo.vespa.hosted.provision.Node; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -18,11 +22,13 @@ import java.util.stream.Collectors; * @author dybis */ public class NodeRepoMock implements NodeRepository { - private List<ContainerNodeSpec> containerNodeSpecs = new ArrayList<>(); - private final CallOrderVerifier callOrderVerifier; private static final Object monitor = new Object(); + private List<ContainerNodeSpec> containerNodeSpecs = new ArrayList<>(); + private final Map<String, List<ContainerAclSpec>> acls = new HashMap<>(); + private final CallOrderVerifier callOrderVerifier; + public NodeRepoMock(CallOrderVerifier callOrderVerifier) { this.callOrderVerifier = callOrderVerifier; } @@ -44,6 +50,14 @@ public class NodeRepoMock implements NodeRepository { } @Override + public List<ContainerAclSpec> getContainerAclSpecs(String hostName) { + synchronized (monitor) { + return Optional.ofNullable(acls.get(hostName)) + .orElseGet(Collections::emptyList); + } + } + + @Override public void updateNodeAttributes(String hostName, NodeAttributes nodeAttributes) { synchronized (monitor) { callOrderVerifier.add("updateNodeAttributes with HostName: " + hostName + ", " + nodeAttributes); @@ -96,4 +110,14 @@ public class NodeRepoMock implements NodeRepository { return containerNodeSpecs.size(); } } + + public void addContainerAclSpecs(String hostname, List<ContainerAclSpec> containerAclSpecs) { + synchronized (monitor) { + if (this.acls.containsKey(hostname)) { + this.acls.get(hostname).addAll(containerAclSpecs); + } else { + this.acls.put(hostname, containerAclSpecs); + } + } + } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java new file mode 100644 index 00000000000..d3849deeb67 --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/AclMaintainerTest.java @@ -0,0 +1,110 @@ +package com.yahoo.vespa.hosted.node.admin.maintenance; + +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; +import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.integrationTests.CallOrderVerifier; +import com.yahoo.vespa.hosted.node.admin.integrationTests.NodeRepoMock; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AclMaintainerTest { + + private static final String NODE_ADMIN_HOSTNAME = "node-admin"; + + private AclMaintainer aclMaintainer; + private DockerOperations dockerOperations; + private NodeRepoMock nodeRepository; + + @Before + public void before() { + this.dockerOperations = mock(DockerOperations.class); + this.nodeRepository = new NodeRepoMock(new CallOrderVerifier()); + this.aclMaintainer = new AclMaintainer(dockerOperations, nodeRepository, () -> NODE_ADMIN_HOSTNAME); + } + + @Test + public void configures_container_acl() { + Container container = makeContainer("container-1"); + List<ContainerAclSpec> aclSpecs = makeAclSpecs(3, container.name); + nodeRepository.addContainerAclSpecs(NODE_ADMIN_HOSTNAME, aclSpecs); + aclMaintainer.run(); + assertAclsApplied(container.name, aclSpecs); + } + + @Test + public void does_not_configure_acl_for_stopped_container() { + Container stoppedContainer = makeContainer("container-1", false); + nodeRepository.addContainerAclSpecs(NODE_ADMIN_HOSTNAME, makeAclSpecs(1, stoppedContainer.name)); + aclMaintainer.run(); + verify(dockerOperations, never()).executeCommandInNetworkNamespace(any(), any()); + } + + @Test + public void rollback_is_attempted_when_applying_acl_fail() { + Container container = makeContainer("container-1"); + nodeRepository.addContainerAclSpecs(NODE_ADMIN_HOSTNAME, makeAclSpecs(1, container.name)); + + doThrow(new RuntimeException("iptables command failed")) + .doNothing() + .when(dockerOperations) + .executeCommandInNetworkNamespace(any(), any()); + + aclMaintainer.run(); + + verify(dockerOperations).executeCommandInNetworkNamespace( + eq(container.name), + aryEq(new String[]{"ip6tables", "-P", "INPUT", "ACCEPT"}) + ); + } + + private void assertAclsApplied(ContainerName containerName, List<ContainerAclSpec> containerAclSpecs) { + verify(dockerOperations).executeCommandInNetworkNamespace( + eq(containerName), + aryEq(new String[]{"ip6tables", "-F", "INPUT"}) + ); + containerAclSpecs.forEach(aclSpec -> verify(dockerOperations).executeCommandInNetworkNamespace( + eq(containerName), + aryEq(new String[]{"ip6tables", "-A", "INPUT", "-s", aclSpec.ipAddress(), "-j", "ACCEPT"}) + )); + verify(dockerOperations).executeCommandInNetworkNamespace( + eq(containerName), + aryEq(new String[]{"ip6tables", "-P", "INPUT", "DROP"}) + ); + } + + private Container makeContainer(String hostname) { + return makeContainer(hostname, true); + } + + private Container makeContainer(String hostname, boolean running) { + final Container container = new Container(hostname, new DockerImage("mock"), + new ContainerName(hostname), running); + when(dockerOperations.getContainer(eq(hostname))).thenReturn(Optional.of(container)); + return container; + } + + private static List<ContainerAclSpec> makeAclSpecs(int count, ContainerName containerName) { + return IntStream.rangeClosed(1, count) + .mapToObj(i -> new ContainerAclSpec("node-" + i, "::" + i, + containerName.asString())) + .collect(Collectors.toList()); + } + +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java index a068ffc09f2..a7c5703f4cc 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminImplTest.java @@ -51,7 +51,7 @@ public class NodeAdminImplTest { final Function<String, NodeAgent> nodeAgentFactory = mock(NodeAgentFactory.class); final NodeAdminImpl nodeAdmin = new NodeAdminImpl(dockerOperations, nodeAgentFactory, Optional.empty(), 100, - new MetricReceiverWrapper(MetricReceiver.nullImplementation)); + new MetricReceiverWrapper(MetricReceiver.nullImplementation), Optional.empty()); final NodeAgent nodeAgent1 = mock(NodeAgentImpl.class); final NodeAgent nodeAgent2 = mock(NodeAgentImpl.class); |