#!/usr/bin/python3
# This file is part of Cockpit.
#
# Copyright (C) 2014 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import json

import parent
from testlib import *


@skipPackage("cockpit-docker")
@skipImage("No docker packaged", "rhel-8-2", "rhel-8-2-distropkg", "centos-8-stream")
@skipImage("Not supporting cockpit-docker on CoreOS", "fedora-coreos")
@skipImage("Not supporting cockpit-docker on Debian >= 11 and Ubuntu >= 20.04", "debian-testing", "ubuntu-2004")
@skipImage("Docker not available", "fedora-31", "fedora-testing", "fedora-32") # https://github.com/cockpit-project/cockpit/issues/12670
class TestDocker(MachineCase):

    def setUp(self):
        super().setUp()
        m = self.machine

        m.execute("systemctl start docker || systemctl start docker-latest")

    def confirm(self):
        b = self.browser
        b.wait_popup("confirmation-dialog")
        b.click("#confirmation-dialog-confirm")
        # popdown should be really quick
        with b.wait_timeout(5):
            b.wait_popdown("confirmation-dialog")

    def deleteImageConfirm(self):
        b = self.browser
        b.wait_popup('delete-image-confirmation-dialog')
        b.click('#delete-image-confirmation-dialog-confirm')
        # popdown could be delayed because of running containers
        with b.wait_timeout(20):
            b.wait_popdown('delete-image-confirmation-dialog')

    def del_container_from_details(self, container_name):
        b = self.browser
        b.click("#container-details-delete")
        with b.wait_timeout(20):
            try:
                self.confirm()
                b.wait_not_in_text("#containers-containers", container_name)
            except Error:
                # we may have to confirm again for forced deletion
                self.confirm()
                b.wait_not_in_text("#containers-containers", container_name)

    def wait_for_message(self, message):
        b = self.browser
        # sometimes container output is missing from the logs
        # https://github.com/docker/docker/issues/10617
        b.wait_in_text("#container-terminal", message)

    def testBasic(self):
        b = self.browser
        m = self.machine

        m.execute("systemctl start docker || systemctl start docker-latest")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        message = "HelloMessage."

        # show all containers to interact with stopped ones
        b.wait_in_text("#containers-containers-filter", "Images and running containers")
        b.set_val("#containers-containers-filter", "all")
        b.wait_in_text("#containers-containers-filter", "Everything")

        # Run it
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE1")
        b.set_val("#containers-run-image-command", """/bin/sh -c 'echo "%s"; sleep 5' """ % message)
        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE1")
        b.wait_in_text("#containers-containers", "busybox:latest")

        # Check output of the probe
        b.click('#containers-containers tr:contains("PROBE1")')
        b.wait_visible('#container-details')
        # The terminal uses NO-BREAK SPACE so we check for that here
        self.wait_for_message(message)
        b.wait_in_text("#container-details-state", "Exited")

        self.del_container_from_details("PROBE1")

        # Run it without TTY and make sure it keeps running so that we
        # can link to it.
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE1")
        b.set_val("#containers-run-image-command", "/bin/sh -c 'echo \"%s\"; sleep 10000'" % (message))
        b.click("#containers-run-image-with-terminal")
        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE1")

        # Check not linked
        info = json.loads(m.execute('docker inspect PROBE1'))
        self.assertEqual(info[0]["HostConfig"]["Links"], None)

        # Create another container linked to old one
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE2")
        b.set_val("#containers-run-image-command", "/bin/echo -e '%s\n%s\n'" % (message, message))
        b.click("#containers-run-image-with-terminal")
        b.wait_visible("#select-linked-containers:empty")
        b.click("#link-containers")
        b.wait_visible("#select-linked-containers:parent")
        b.click("#select-linked-containers form:last-child .link-container button")
        b.click("#select-linked-containers form:last-child .link-container a[value='PROBE1']")
        b.set_val("#select-linked-containers form:last-child  input[name='alias']", "alias1")
        b.click("#select-linked-containers form:last-child  button.plus-button")
        # new line should be the second group
        b.wait_present("#select-linked-containers .form-group:eq(1)")
        b.click("#select-linked-containers form:last-child .link-container button")
        b.click("#select-linked-containers form:last-child .link-container a[value='PROBE1']")
        b.set_val("#select-linked-containers form:last-child  input[name='alias']", "alias2")
        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE2")

        # Wait for PROBE2 to Exit
        b.click('#containers-containers tr:contains("PROBE2")')
        b.wait_in_text("#container-details-state", "Exited")

        # Check links
        info = json.loads(m.execute('docker inspect PROBE2'))
        self.assertEqual(set(info[0]["HostConfig"]["Links"]),
                         set(["/PROBE1:/PROBE2/alias1", "/PROBE1:/PROBE2/alias2"]))

        # delete PROBE2 from dash
        b.click("#container-details .content-filter a")
        b.wait_visible("#containers-containers")
        b.wait_in_text('#containers-containers', "PROBE2")
        b.click('#containers-containers tbody tr:contains("PROBE2") td.listing-ct-toggle')
        b.click('#containers-containers tbody tr:contains("PROBE2") + tr button.btn-delete')
        with b.wait_timeout(20):
            try:
                b.wait_not_in_text('#containers-containers', "PROBE2")
            except Error:
                self.confirm()
                b.wait_not_in_text('#containers-containers', "PROBE2")

        # Check output of PROBE1
        b.click('#containers-containers tr:contains("PROBE1")')
        b.wait_visible("#container-details")
        b.wait_in_text('#container-details', "busybox:latest")
        self.wait_for_message(message)
        b.call_js_func("ph_count_check", ".console-ct pre", 1)
        b.click("#container-details-stop")
        b.wait_in_text("#container-details-state", "Exited")

        # Make sure after restart we only have one
        b.click("#container-details-start")
        self.wait_for_message("%s\n%s" % (message, message))
        b.call_js_func("ph_count_check", ".console-ct pre", 1)
        b.click("#container-details-stop")
        b.wait_in_text("#container-details-state", "Exited")

        # Delete the container from the image-details page
        b.click("#container-details .content-filter a")
        b.click('#containers-images tr:contains("busybox:latest")')
        b.wait_visible("#image-details")
        b.wait_present(".image-details-used")
        b.wait_visible(".image-details-used tr td:first-child")
        b.click(".image-details-used .enable-danger")
        b.click(".image-details-used .btn-delete")
        b.wait_not_present(".image-details-used tr td")

        # Delete image itself
        b.click("#image-details-delete")
        self.deleteImageConfirm()
        b.wait_visible("#containers")
        b.wait_not_in_text('#containers-images', 'busybox:latest')

    def testDeleteImages(self):
        b = self.browser
        m = self.machine
        m.execute("systemctl start docker || systemctl start docker-latest")
        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        # Deleting an image without depending containers has no container list
        b.click('#containers-images tr:contains("busybox:latest") td.listing-ct-toggle')
        b.click('#containers-images tr:contains("busybox:latest") + tr .btn-delete')
        b.wait_popup('delete-image-confirmation-dialog')
        self.assertFalse(b.is_visible('#delete-image-confirmation-dialog-containers'))
        b.click('#delete-image-confirmation-dialog-cancel')
        b.wait_popdown('delete-image-confirmation-dialog')
        b.click('#containers-images tr:contains("busybox:latest") td.listing-ct-toggle')

        # Create some containers
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup('containers_run_image_dialog')
        b.set_val('#containers-run-image-name', 'C1')
        b.click('#containers-run-image-run')
        b.wait_popdown('containers_run_image_dialog')
        b.wait_in_text('#containers-containers', 'C1')
        b.wait_in_text('#containers-containers', 'busybox:latest')
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup('containers_run_image_dialog')
        b.set_val('#containers-run-image-name', 'C2')
        b.click('#containers-run-image-run')
        b.wait_popdown('containers_run_image_dialog')
        b.wait_in_text('#containers-containers', 'C2')
        b.wait_in_text('#containers-containers', 'busybox:latest')

        # Delete image itself without deleting depending containers
        b.click('#containers-images tr:contains("busybox:latest")')
        b.click('#image-details-delete')
        b.wait_popup('delete-image-confirmation-dialog')
        b.wait_visible('#delete-image-confirmation-dialog-containers')

        # Delete image with running containers
        b.click('#image-details-delete')
        b.wait_popup('delete-image-confirmation-dialog')
        b.wait_in_text('#delete-image-confirmation-dialog-confirm', 'Stop and delete')
        self.deleteImageConfirm()
        b.wait_visible('#containers')
        b.wait_not_present('#containers-images tr:contains("busybox:latest")')

    def testExpose(self):
        b = self.browser
        m = self.machine
        self.allow_journal_messages('.*denied.*name_connect.*docker.*')

        m.execute("systemctl start docker || systemctl start docker-latest")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        port = 3380
        message = "HelloMessage."

        # Create an updated image, expose a port
        m.execute("mkdir /var/tmp/container-probe")
        m.upload(["verify/files/listen-on-port.sh"], "/var/tmp/container-probe")
        m.execute("""echo -e '
FROM busybox
MAINTAINER cockpit
EXPOSE %(port)d
ADD listen-on-port.sh /listen-on-port.sh
CMD ["/listen-on-port.sh", "%(port)d", "%(message)s"]
' > /var/tmp/container-probe/Dockerfile""" % {'message': message, 'port': port})
        image_name = "test/container-probe"
        m.execute("docker build -t %s /var/tmp/container-probe" % (image_name))
        m.execute("rm -rf /var/tmp/container-probe")

        # Wait for it to appear
        b.wait_in_text("#containers-images", image_name)

        nport = port + 1

        # Run it and expose additional port via the ui
        b.click("#containers-images tr:contains(\"%s\") button.play-button" % (image_name))
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE2")
        # Tell our probe to listen on both ports
        second_message = "%s" % (message)
        b.set_val("#containers-run-image-command",
                  ("/bin/sh -c \"/listen-on-port.sh %(port)d %(message)s; " +
                   "/listen-on-port.sh %(second_port)d %(second_message)s\"")
                  % {'port': port,
                     'message': message,
                     'second_port': nport,
                     'second_message': second_message})
        b.set_val(".containers-run-portmapping form:eq(0) input:eq(1)", port)
        b.click(".containers-run-portmapping form:eq(0) .plus-button")
        b.set_val(".containers-run-portmapping form:eq(1) input:eq(0)", nport)
        b.set_val(".containers-run-portmapping form:eq(1) input:eq(1)", nport)

        # Add a mount point
        b.click("#mount-volumes")
        b.set_val("#select-mounted-volumes form:eq(0) input:eq(0)", "/blah")
        b.set_val("#select-mounted-volumes form:eq(0) input:eq(1)", "/mnt")

        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE2")

        # Check output of the probe
        b.click('#containers-containers tr:contains("PROBE2")')
        b.wait_in_text("#container-details-ports", ":%d -> %d/tcp" % (port, port))
        b.wait_in_text("#container-details-ports", ":%d -> %d/tcp" % (nport, nport))
        b.wait_in_text("#container-details-volumes", "/mnt:/blah")
        self.wait_for_message(message)

        # Check connection on first port, remove trailing whitespace
        data = m.execute("exec 3<>/dev/tcp/localhost/%d; cat <&3" % (port)).rstrip()
        self.assertEqual(data, message)

        self.wait_for_message(second_message)

        # Check connection on second port, remove trailing whitespace
        data = m.execute("exec 3<>/dev/tcp/localhost/%d; cat <&3" % (nport)).rstrip()
        self.assertEqual(data, second_message)

        # Wait for exit
        b.wait_in_text("#container-details-state", "Exited")

        self.del_container_from_details("PROBE2")

    def testVolumesAndEnvironment(self):
        b = self.browser
        m = self.machine

        m.execute("systemctl start docker || systemctl start docker-latest")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        # Select to view all running containers
        b.set_val("#containers-containers-filter", "all")

        # Create an updated image, add environment variable
        m.execute("mkdir /var/tmp/container-probe")
        m.execute("""echo -e '
FROM busybox
ENV zero=GGG
CMD ["/bin/sh"]
' > /var/tmp/container-probe/Dockerfile""")
        image_name = "test/container-probe"
        m.execute("docker build -t %s /var/tmp/container-probe" % (image_name))
        m.execute("rm -rf /var/tmp/container-probe")

        # Wait for it to appear
        b.wait_in_text("#containers-images", image_name)

        # Run it and add another environment variable via the ui
        b.click("#containers-images tr:contains(\"%s\") button.play-button" % (image_name))
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "PROBE4")
        b.set_val("#containers-run-image-command", "/bin/sh")

        # Two environment variables
        b.wait_val("#select-claimed-envvars form:eq(1) input:eq(0)", "zero")
        b.wait_val("#select-claimed-envvars form:eq(1) input:eq(1)", "GGG")
        b.click("#select-claimed-envvars form:eq(0) .plus-button")
        b.set_val("#select-claimed-envvars form:eq(2) input:eq(0)", "SECOND")
        b.set_val("#select-claimed-envvars form:eq(2) input:eq(1)", "marmalade")

        # And a mount point
        b.click("#mount-volumes")
        b.set_val("#select-mounted-volumes form:eq(0) input:eq(0)", "/blah")
        b.set_val("#select-mounted-volumes form:eq(0) input:eq(1)", "/mnt")
        if "debian" not in m.image and "ubuntu" not in m.image:
            m.execute("chcon --dereference -HRt svirt_sandbox_file_t /mnt")
        m.execute("touch /mnt/dripping.txt")

        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")
        b.wait_in_text("#containers-containers", "PROBE4")

        # Check output of the probe
        b.click('#containers-containers tr:contains("PROBE4")')
        b.wait_visible("#container-details")

        b.wait_present("#container-terminal")
        b.wait_present("#container-terminal .console-ct")
        b.wait_present("#container-terminal .console-ct .terminal")

        b.focus('#container-terminal .console-ct .terminal')

        # Wait for the container to wake up
        try:
            b.key_press("\r\r\r")
            b.wait_in_text("#container-terminal", "#")
        except Error as ex:
            if not ex.msg.startswith('timeout'):
                raise

        b.key_press("clear\r")
        b.wait_in_text("#container-terminal", "#")

        b.focus('#container-terminal .console-ct .terminal')
        b.key_press("env\r")
        b.wait_in_text("#container-terminal", "zero=GGG")
        b.wait_in_text("#container-terminal", "SECOND=marmalade")

        b.focus('#container-terminal .console-ct .terminal')
        b.key_press("ls /blah/\r")
        b.wait_in_text("#container-terminal", "dripping.txt")

        self.allow_journal_messages('.*denied.*name_connect.*docker.*')

    def testFailure(self):
        b = self.browser
        m = self.machine

        # Try to access docker without admin (wheel, sudo, ...) group
        # also need to disable Ubuntu's legacy "admin" sudo group as it happens
        # to have the same name as our admin test user
        m.execute("usermod -G '' admin; sed -i '/^%admin/d' /etc/sudoers")

        self.allow_journal_messages("http:///var/run/docker.sock/.*: couldn't connect: .*")

        self.login_and_go("/docker", authorized=False)

        # We can not become root, so we can't access docker.
        b.wait_text("#curtain h1", "Not authorized to access Docker on this system")
        b.wait_visible("#curtain button[data-action='docker-connect']")

        # Give "admin" access via the admin group and login again
        m.execute("usermod -G %s admin" % m.get_admin_group())
        m.execute("systemctl stop docker")
        b.relogin("/docker", authorized=True)
        self.allow_restart_journal_messages()

        # Now we can become root and start docker
        b.wait_text("#curtain h1", "Docker is not installed or activated on the system")
        b.click("#curtain button[data-action='docker-start']")
        with b.wait_timeout(120):
            b.wait_visible("#containers-containers")

        self.allow_authorize_journal_messages()
        self.allow_journal_messages("cannot reauthorize identity.*:.*unix-user:root.*")
        self.allow_journal_messages("cannot reauthorize identity.*:.*unix-user:builder.*")

    def testRestart(self):
        b = self.browser
        m = self.machine

        # This happens due to the Docker restart
        self.allow_journal_messages('.*received truncated HTTP response.*')

        m.execute("systemctl start docker || systemctl start docker-latest")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        # Run one container without restarting
        b.wait_visible("#containers-images")
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "NORESTART")
        b.set_val("#containers-run-image-command", "/bin/sh -c 'echo \"no-restart\"; sleep 10000'")
        b.click("#containers-run-image-with-terminal")
        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")

        # Run another container with restarting
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "YAYRESTART")
        b.set_val("#containers-run-image-command", "/bin/sh -c 'echo \"yay-restart\"; sleep 10000'")
        b.click("#containers-run-image-with-terminal")
        b.click("#restart-policy-select button")
        b.click("#restart-policy-select a[data-value=always]")
        b.wait_in_text("#restart-policy-select button", "Always")
        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")

        # Make sure they've started
        b.wait_in_text("#containers-containers", "NORESTART")
        b.wait_in_text("#containers-containers", "YAYRESTART")

        # Restart docker, docker.socket needs special handling
        b.logout()
        m.execute("(systemctl kill --signal=9 docker-containerd && systemctl restart docker-containerd ) || systemctl restart docker || systemctl restart docker-latest")
        self.login_and_go("/docker")

        # show all containers to interact with stopped ones
        with b.wait_timeout(120):
            b.wait_visible("#containers-containers")
        b.set_val("#containers-containers-filter", "all")

        # Now check that one restarts
        b.wait_in_text("#containers-images", "busybox:latest")
        b.wait_present('#containers-containers tr:contains("YAYRESTART") td:contains("running")')
        b.wait_present('#containers-containers tr:contains("NORESTART") td:contains("exited")')

    def testResources(self):
        b = self.browser
        m = self.machine

        m.execute("systemctl start docker || systemctl start docker-latest")

        self.login_and_go("/docker")
        b.wait_in_text("#containers-images", "busybox:latest")

        # Run one container without restarting
        b.click('#containers-images tr:contains("busybox:latest") button.play-button')
        b.wait_popup("containers_run_image_dialog")
        b.set_val("#containers-run-image-name", "RESOURCE")
        b.set_val("#containers-run-image-command", "/bin/sleep 1000000")
        b.click("#containers-run-image-with-terminal")
        b.click("#containers-run-image-memory input[type='checkbox']")
        b.set_val("#containers-run-image-memory input.size-text-ct", "236")
        b.click("#containers-run-image-cpu input[type='checkbox']")
        b.set_val("#containers-run-image-cpu input.size-text-ct", "512")
        b.click("#containers-run-image-run")
        b.wait_popdown("containers_run_image_dialog")

        # Make sure they've started
        b.wait_in_text("#containers-containers", "RESOURCE")

        # Check output of PROBE1
        b.click('#containers-containers tr:contains("RESOURCE")')
        b.wait_in_text("#container-details-memory-row", "/ 236 MiB")
        b.wait_in_text("#container-details-cpu-row", "512 shares")

        # Now adjust things
        b.click("#container-details-resource-row button")
        b.wait_popup("container-resources-dialog")
        b.set_val(".memory-slider input.size-text-ct", "246")
        b.set_val(".cpu-slider input.size-text-ct", "256")
        b.click("#container-resources-dialog .pf-m-primary")
        b.wait_popdown("container-resources-dialog")

        # This should be updated
        b.wait_in_text("#container-details-memory-row", "/ 246")
        b.wait_in_text("#container-details-cpu-row", "256 shares")

        # Remove the restrictions
        b.click("#container-details-resource-row button")
        b.wait_popup("container-resources-dialog")
        b.click(".memory-slider input[type='checkbox']")
        b.click(".cpu-slider input[type='checkbox']")
        b.click("#container-resources-dialog .pf-m-primary")
        b.wait_popdown("container-resources-dialog")

        # This should be updated
        b.wait_in_text("#container-details-cpu-row", "1024 shares")

    @skipImage("ABRT not available", "debian-stable", "debian-testing", "ubuntu-stable",
               "ubuntu-1804", "ubuntu-2004", "rhel-8-2", "rhel-8-2-distropkg")
    def testContainerProblems(self):
        b = self.browser
        m = self.machine

        m.execute("systemctl start docker || systemctl start docker-latest")

        # start container
        m.execute('docker run -dit --name crashing_container busybox /bin/sh')

        # crash in container
        m.execute('docker exec crashing_container sh -c "sleep 5 & sleep 1; pkill -ABRT sleep"')
        self.allow_journal_messages('Process.*sleep.*dumped core.*')
        self.allow_journal_messages('Stack trace.*')
        self.allow_journal_messages('#0.*sleep.*')

        # login and go to docker page (Docker Containers)
        self.login_and_go("/docker")

        sel = "#containers-containers .listing-ct-item .listing-ct-toggle"
        b.click(sel)

        # wait for the Problems tab then click it
        sel = '#containers-containers .listing-ct-panel .listing-ct-head ul li a:contains("Problems")'
        b.click(sel)

        # open link to logs
        sel = "#containers-containers .listing-ct-panel .listing-ct-body a.list-group-item"
        b.wait_in_text(sel, "sleep")
        b.click(sel + ':contains("sleep")')

        # wait for the page to load
        b.enter_page("/system/logs")

        # check for abrtd service in text
        b.wait_in_text("#journal-entry-heading", "sleep")
        b.wait_in_text("#journal-entry-fields", "abrtd.service")


if __name__ == '__main__':
    test_main()
