// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.maintenance.acl;

import com.yahoo.collections.Pair;
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.maintenance.acl.iptables.Action;
import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Chain;
import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Command;
import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.FlushCommand;
import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.PolicyCommand;
import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository;
import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.
 * <p>
 * If an ACL 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.
 * <p>
 * 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 static final String IPTABLES_COMMAND = "ip6tables";

    private final DockerOperations dockerOperations;
    private final NodeRepository nodeRepository;
    private final String nodeAdminHostname;
    private final Map<ContainerName, Acl> containerAcls;

    public AclMaintainer(DockerOperations dockerOperations, NodeRepository nodeRepository,
                         String nodeAdminHostname) {
        this.dockerOperations = dockerOperations;
        this.nodeRepository = nodeRepository;
        this.nodeAdminHostname = nodeAdminHostname;
        this.containerAcls = new HashMap<>();
    }

    private boolean isAclActive(ContainerName containerName, Acl acl) {
        return Optional.ofNullable(containerAcls.get(containerName))
                .map(acl::equals)
                .orElse(false);
    }

    private void applyAcl(ContainerName containerName, Acl acl) {
        if (isAclActive(containerName, acl)) {
            return;
        }
        final Command flush = new FlushCommand(Chain.INPUT);
        final Command rollback = new PolicyCommand(Chain.INPUT, Action.ACCEPT);
        log.info("Start modifying ACL rules");
        try {
            log.info("Running ACL command '" + flush.asString() + "'");
            dockerOperations.executeCommandInNetworkNamespace(containerName, flush.asArray(IPTABLES_COMMAND));
            acl.toCommands().forEach(command -> {
                log.info("Running ACL command '" + command.asString() + "'");
                dockerOperations.executeCommandInNetworkNamespace(containerName,
                                                                  command.asArray(IPTABLES_COMMAND));
            });
            containerAcls.put(containerName, acl);
        } catch (Exception e) {
            log.error("Exception occurred while configuring ACLs, attempting rollback", e);
            try {
                dockerOperations.executeCommandInNetworkNamespace(containerName, rollback.asArray(IPTABLES_COMMAND));
            } catch (Exception ne) {
                log.error("Rollback failed, giving up", ne);
            }
        }
        log.info("Finished modifying ACL rules");
    }

    private synchronized void configureAcls() {
        final Map<ContainerName, List<ContainerAclSpec>> aclSpecsGroupedByContainerName = nodeRepository
                .getContainerAclSpecs(nodeAdminHostname).stream()
                .collect(Collectors.groupingBy(ContainerAclSpec::trustedBy));

        dockerOperations
                .getAllManagedContainers().stream()
                .filter(container -> container.state.isRunning())
                .map(container -> new Pair<>(container, aclSpecsGroupedByContainerName.get(container.name)))
                .filter(pair -> pair.getSecond() != null)
                .forEach(pair ->
                        applyAcl(pair.getFirst().name, new Acl(pair.getFirst().pid, pair.getSecond())));
    }

    @Override
    public void run() {
        try {
            configureAcls();
        } catch (Throwable t) {
            log.error("Failed to configure ACLs", t);
        }
    }
}

@@ -3043,25 +3043,57 @@
 ng ACL rules
-%22
+ for %22 + containerName.asString()
 );%0A        t
@@ -3396,33 +3396,65 @@
 .asString() + %22'
-%22
+ for %22 + containerName.asString()
 );%0A             
@@ -3786,16 +3786,53 @@
 ing ACLs
+ for %22 + containerName.asString() + %22
 , attemp
@@ -4059,16 +4059,61 @@
 Rollback
+ of ACLs for %22 + containerName.asString() + %22
  failed,
@@ -4200,17 +4200,49 @@
 CL rules
-%22
+ for %22 + containerName.asString()
 );%0A    %7D

Add container name to log messages