Пример #1
0
    def install_network(self) -> None:
        """Installs the Orchest Docker network."""
        # Don't install the network again if it is already installed
        # because that will create the another network with the same
        # name but with another ID. Thereby, breaking Orchest.
        try:
            is_installed = self.is_network_installed()
        except docker.errors.APIError:
            # TODO: reraise the error but with a helpful message that
            # helps the user fix the issue.
            raise

        if not is_installed:
            # We only want to print this message to the user once. The
            # best bet is that if the Orchest network has not yet been
            # installed, then most likely the user has not seen this
            # message before.
            utils.echo(
                "Orchest sends anonymized telemetry to analytics.orchestapp.com."
                " To disable it, please refer to:", )
            utils.echo(
                "\thttps://orchest.readthedocs.io/en/stable/user_guide/other.html#configuration",  # noqa: E501, W505
                wrap=100,
            )

            self.docker_client.install_network(self.network)
Пример #2
0
    async def _pull_images(
        self,
        images: Iterable[str],
        prog_bar: bool = True,
        force: bool = False,
    ):
        pulls = [self._pull_image(image, force=force) for image in images]

        if prog_bar:
            pulls = tqdm.as_completed(
                pulls,
                total=len(pulls),
                ncols=WRAP_LINES,
                desc="Pulling images",
                ascii=True,
                position=0,
                leave=True,
                bar_format="{desc}: {n}/{total}|{bar}|",
            )

        for pull in pulls:
            await pull

        if prog_bar:
            pulls.close()  # type: ignore

            # Makes the next echo start on the line underneath the
            # status bar instead of after.
            await asyncio.sleep(0.05)
            # TODO: Check whether we can use print(flush=True) here to
            #       make this class not dependend on utils.
            utils.echo()

        await self.close_aclient()
Пример #3
0
def update_container_config_with_cloud(container_config: dict,
                                       env: Optional[dict] = None) -> None:
    """Updates the container config to run with --cloud.

    Args:
        container_config: An existing container config, to be updated in
            place.
        env: Refer to :meth:`get_container_config`.
    """
    if env is None:
        env = utils.get_env()

    utils.echo(
        "Starting Orchest with --cloud. Some GUI functionality is altered.")

    cloud_inject = {
        "orchest-webserver": {
            "Env": [
                "CLOUD=true",
            ],
        },
        "update-server": {
            "Env": [
                "CLOUD=true",
            ],
        },
        "auth-server": {
            "Env": [
                "CLOUD=true",
            ],
        },
    }

    inject_dict(container_config, cloud_inject, overwrite=False)
Пример #4
0
def adduser(
    username: str = typer.Argument(..., help="Name of the user to add."),
    set_token: bool = typer.Option(
        False,
        "--set-token",
        help="True if the token of the user is to be setup."),
    is_admin: bool = typer.Option(
        False,
        show_default="--no-is-admin",
        help=("If the newly created user should be an admin or not. An admin"
              " user cannot be deleted."),
    ),
    non_interactive: bool = typer.Option(
        False,
        hidden=True,
        help="Use non interactive mode for password and token."),
    non_interactive_password: Optional[str] = typer.Option(
        None, hidden=True, help="User password in non interactive mode."),
    non_interactive_token: Optional[str] = typer.Option(
        None, hidden=True, help="User token in non interactive mode."),
):
    """
    Add user to Orchest.
    """
    if non_interactive:
        password = non_interactive_password
        token = non_interactive_token
    else:
        if non_interactive_password:
            echo("Cannot use non_interactive_password in interactive mode.",
                 err=True)
            raise typer.Exit(code=1)
        if non_interactive_token:
            echo("Cannot use non_interactive_token in interactive mode.",
                 err=True)
            raise typer.Exit(code=1)

        password = typer.prompt("Password",
                                hide_input=True,
                                confirmation_prompt=True)
        if set_token:
            token = typer.prompt("Token",
                                 hide_input=True,
                                 confirmation_prompt=True)
        else:
            token = None

    if password is None:
        echo("Password must be specified.", err=True)
        raise typer.Exit(code=1)
    elif not password:
        echo("Password cannot be empty.", err=True)
        raise typer.Exit(code=1)
    if token is not None and not token:
        echo("Token cannot be empty.", err=True)
        raise typer.Exit(code=1)

    app.add_user(username, password, token, is_admin)
Пример #5
0
    def install(self, language: str, gpu: bool = False):
        """Installs Orchest for the given language.

        Pulls all the Orchest containers necessary to run all the
        features for the given `language`.

        """
        self.resource_manager.install_network()

        # Check whether the install is complete.
        pulled_images = self.resource_manager.get_images()
        req_images = get_required_images(language, gpu)
        missing_images = set(req_images) - set(pulled_images)

        if not missing_images:
            utils.echo("Installation is already complete. Did you mean to run:")
            utils.echo("\torchest update")
            return

        utils.echo("Installing Orchest...")
        logger.info("Pulling images:\n" + "\n".join(missing_images))
        self.docker_client.pull_images(missing_images, prog_bar=True)

        utils.echo(
            "Checking whether all containers are running the same version of Orchest."
        )
        self.version(ext=True)
Пример #6
0
    def stop(self, skip_containers: Optional[List[str]] = None):
        """Stop the Orchest application.

        Args:
            skip_containers: The names of the images of the containers
                for which the containers are not stopped.

        """

        ids, running_containers = self.resource_manager.get_containers(
            state="running")
        if not self.is_running(running_containers):
            utils.echo("Orchest is not running.")
            return

        # Exclude the orchest-ctl from shutting down itself.
        if skip_containers is None:
            skip_containers = []
        skip_containers += ["orchest/orchest-ctl:latest"]

        ids: Tuple[str]
        running_containers: Tuple[Optional[str]]
        ids, running_containers = list(
            zip(*[(id_, c) for id_, c in zip(ids, running_containers)
                  if c not in skip_containers]))

        utils.echo("Shutting down...")
        logger.info("Shutting down containers:\n" +
                    "\n".join(running_containers))

        self.docker_client.remove_containers(ids)
        utils.echo("Shutdown successful.")
Пример #7
0
    def stop(self, skip_containers: Optional[List[str]] = None):
        """Stop the Orchest application.

        Args:
            skip_containers: The names of the images of the containers
                for which the containers are not stopped.

        """

        ids, running_containers = self.resource_manager.get_containers(state="running")
        if not utils.is_orchest_running(running_containers):
            utils.echo("Orchest is not running.")
            return

        # Exclude the orchest-ctl from shutting down itself.
        if skip_containers is None:
            skip_containers = []
        skip_containers += ["orchest/orchest-ctl:latest"]

        utils.echo("Shutting down...")
        # This is necessary because some of our containers might spawn
        # other containers, leading to a possible race condition where
        # the listed running containers are not up to date with the
        # real state anymore.
        n = 2
        for _ in range(n):

            id_containers = [
                (id_, c)
                for id_, c in zip(ids, running_containers)
                if c not in skip_containers
            ]
            # It might be that there are no containers to shut down
            # after filtering through skip_containers.
            if not id_containers:
                break
            ids: Tuple[str]
            running_containers: Tuple[Optional[str]]
            ids, running_containers = list(zip(*id_containers))

            logger.info("Shutting down containers:\n" + "\n".join(running_containers))
            self.docker_client.remove_containers(ids)

            # This is a safeguard against the fact that docker might be
            # buffering the start of a container, which translates to
            # the fact that we could "miss" the container and leave it
            # dangling. See #239 for more info.
            time.sleep(2)
            ids, running_containers = self.resource_manager.get_containers(
                state="running"
            )
            if not ids:
                break

        utils.echo("Shutdown successful.")
Пример #8
0
    def status(self):

        _, running_containers = self.resource_manager.get_containers(state="running")

        if not self.is_running(running_containers):
            utils.echo("Orchest is not running.")
            return

        # Minimal set of containers to be running for Orchest to be in
        # a valid state.
        valid_set: Set[str] = reduce(
            lambda x, y: x.union(y), self.on_start_images, set()
        )

        if valid_set - set(running_containers):
            utils.echo("Orchest is running, but has reached an invalid state. Run:")
            utils.echo("\torchest restart")
            logger.warning(
                "Orchest has reached an invalid state. Running containers:\n"
                + "\n".join(running_containers)
            )
        else:
            utils.echo("Orchest is running.")
Пример #9
0
    def add_user(self, username, password, token, is_admin):
        """Adds a new user to Orchest.

        Args:
            username:
            password:
            token:
            is_admin:
        """

        ids, running_containers = self.resource_manager.get_containers(
            state="running")
        auth_server_id = None
        database_running = False
        for id, container in zip(ids, running_containers):
            if "postgres" in container:
                database_running = True
            if "auth-server" in container:
                auth_server_id = id

        if not database_running:
            utils.echo("The orchest-database service needs to be running.",
                       err=True)
            raise typer.Exit(code=1)

        if auth_server_id is None:
            utils.echo("The auth-server service needs to be running.",
                       err=True)
            raise typer.Exit(code=1)

        cmd = f"python add_user.py {username} {password}"
        if token:
            cmd += f" --token {token}"
        if is_admin:
            cmd += " --is_admin"

        exit_code = self.docker_client.exec_runs([(auth_server_id, cmd)])[0]

        if exit_code != 0:
            utils.echo(
                ("Non zero exit code whilst trying to add a user to "
                 f"the auth-server: {exit_code}."),
                err=True,
            )

        raise typer.Exit(code=exit_code)
Пример #10
0
    def status(self, ext=False):

        _, running_containers_names = self.resource_manager.get_containers(
            state="running")

        if not self.is_running(running_containers_names):
            utils.echo("Orchest is not running.")
            raise typer.Exit(code=1)

        # Minimal set of containers to be running for Orchest to be in
        # a valid state.
        valid_set: Set[str] = reduce(lambda x, y: x.union(y), _on_start_images,
                                     set())

        if valid_set - set(running_containers_names):
            utils.echo(
                "Orchest is running, but has reached an invalid state. Run:")
            utils.echo("\torchest restart")
            logger.warning(
                "Orchest has reached an invalid state. Running containers:\n" +
                "\n".join(running_containers_names))
            raise typer.Exit(code=2)
        else:
            utils.echo("Orchest is running.")
            if ext:
                utils.echo("Performing extensive status checks...")
                no_issues = True
                for container, exit_code in health_check(
                        self.resource_manager).items():
                    if exit_code != 0:
                        no_issues = False
                        utils.echo(f"{container} is not ready ({exit_code}).")

                if no_issues:
                    utils.echo("All services are ready.")
                else:
                    raise typer.Exit(code=3)
Пример #11
0
def debug_dump(ext: bool, compress: bool) -> None:

    debug_dump_path = "/tmp/debug-dump"
    os.mkdir(debug_dump_path)

    rmanager = OrchestResourceManager()
    errors = []
    for name, func, args in [
        ("configuration", orchest_config_dump, (debug_dump_path, )),
        (
            "containers version",
            containers_version_dump,
            (
                rmanager,
                debug_dump_path,
            ),
        ),
        ("containers logs", containers_logs_dump, (rmanager, debug_dump_path,
                                                   ext)),
        (
            "running containers",
            running_containers_dump,
            (
                rmanager,
                debug_dump_path,
            ),
        ),
        (
            "health check",
            health_check_dump,
            (
                rmanager,
                debug_dump_path,
            ),
        ),
        (
            "database",
            database_debug_dump,
            (
                rmanager,
                debug_dump_path,
            ),
        ),
        ("celery", celery_debug_dump, (rmanager, debug_dump_path, ext)),
        (
            "webserver",
            webserver_debug_dump,
            (
                rmanager,
                debug_dump_path,
            ),
        ),
    ]:
        try:
            utils.echo(f"Generating debug data: {name}.")
            func(*args)
        except Exception as e:
            utils.echo(f"\tError during generation of debug data: {name}.")
            errors.append((name, e))

    with open(os.path.join(debug_dump_path, "errors.txt"), "w") as file:
        file.write(
            "This is a log of errors that happened during the dump, if any.\n")
        for name, exc in errors:
            file.write(f"{name}: {exc}\n")

    if compress:
        os.system(f"tar -zcf {debug_dump_path}.tar.gz -C {debug_dump_path} .")
        debug_dump_path = f"{debug_dump_path}.tar.gz"

        # relative to orchest repo directory
        host_path = "debug-dump.tar.gz"
        os.system(f"cp {debug_dump_path} /orchest-host/{host_path}")

    else:
        # This is to account for the behaviour of cp when it comes to
        # already exising directory. Otherwise, if "t" already exists,
        # the data we want to copy will become a subdirectory of "t",
        # instead of overwriting its files.
        host_path = "debug-dump/"
        t = "/orchest-host/" + host_path
        os.system(f"mkdir -p {t} && cp -r {debug_dump_path}/* {t}")

    utils.echo(f"Complete! Wrote debug dump to: ./{host_path}")
Пример #12
0
    def start(self, container_config: dict):
        """Starts Orchest.

        Raises:
            ValueError: If the `container_config` does not contain a
                configuration for every image that is supposed to run
                on start.

        """
        # Check whether the minimal set of images is present for Orchest
        # to be started.
        pulled_images = self.resource_manager.get_images()
        req_images: Set[str] = reduce(lambda x, y: x.union(y),
                                      _on_start_images, set())
        missing_images = req_images - set(pulled_images)

        if missing_images or not self.resource_manager.is_network_installed():
            utils.echo(
                "Before starting Orchest, make sure Orchest is installed. Run:"
            )
            utils.echo("\torchest install")
            return

        # Check whether the container config contains the set of
        # required images.
        present_imgs = set(config["Image"]
                           for config in container_config.values())
        if present_imgs < req_images:  # proper subset
            raise ValueError(
                "The container_config does not contain a configuration for "
                " every required image: " + ", ".join(req_images))

        # Orchest is already running
        ids, running_containers = self.resource_manager.get_containers(
            state="running")
        if not (req_images - set(running_containers)):
            # TODO: Ideally this would print the port on which Orchest
            #       is running. (Was started before and so we do not
            #       simply know.)
            utils.echo("Orchest is already running...")
            return

        # Orchest is partially running and thus in an inconsistent
        # state. Possibly the start command was issued whilst Orchest
        # is still shutting down.
        if running_containers:
            utils.echo(
                "Orchest seems to be partially running. Before attempting to start"
                " Orchest, shut the application down first:", )
            utils.echo("\torchest stop")
            return

        # Remove old lingering containers.
        ids, exited_containers = self.resource_manager.get_containers(
            state="exited")
        self.docker_client.remove_containers(ids)

        utils.fix_userdir_permissions()
        logger.info("Fixing permissions on the 'userdir/'.")

        utils.echo("Starting Orchest...")
        logger.info("Starting containers:\n" + "\n".join(req_images))

        # Start the containers in the correct order, keeping in mind
        # dependencies between containers.
        for i, to_start_imgs in enumerate(_on_start_images):
            filter_ = {"Image": to_start_imgs}
            config = spec.filter_container_config(container_config,
                                                  filter=filter_)
            stdouts = self.docker_client.run_containers(config,
                                                        use_name=True,
                                                        detach=True)

            # TODO: Abstract version of when the next set of images can
            #       be started. In case the `on_start_images` has more
            #       stages.
            if i == 0:
                utils.wait_for_zero_exitcode(
                    self.docker_client,
                    stdouts["orchest-database"]["id"],
                    "pg_isready --username postgres",
                )

                utils.wait_for_zero_exitcode(
                    self.docker_client,
                    stdouts["rabbitmq-server"]["id"],
                    ('su rabbitmq -c "/opt/rabbitmq/sbin/rabbitmq-diagnostics '
                     '-q check_port_connectivity"'),
                )

        # Get the port on which Orchest is running.
        nginx_proxy = container_config.get("nginx-proxy")
        if nginx_proxy is not None:
            for port, port_binding in nginx_proxy["HostConfig"][
                    "PortBindings"].items():
                exposed_port = port_binding[0]["HostPort"]
                utils.echo(
                    f"Orchest is running at: http://localhost:{exposed_port}")
Пример #13
0
    def install(self, language: str, gpu: bool = False):
        """Installs Orchest for the given language.

        Pulls all the Orchest containers necessary to run all the
        features for the given `language`.

        """
        self.resource_manager.install_network()

        # Check whether the install is complete.
        pulled_images = self.resource_manager.get_images()
        req_images = get_required_images(language, gpu)
        missing_images = set(req_images) - set(pulled_images)

        if not missing_images:
            utils.echo(
                "Installation is already complete. Did you mean to run:")
            utils.echo("\torchest update")
            return

        # The installation is not yet complete, but some images were
        # already pulled before.
        if pulled_images:
            utils.echo(
                "Some images have been pulled before. Don't forget to run:")
            utils.echo("\torchest update")
            utils.echo(
                "after the installation is finished to ensure that all images are"
                " running the same version of Orchest.", )

        utils.echo("Installing Orchest...")
        logger.info("Pulling images:\n" + "\n".join(missing_images))
        self.docker_client.pull_images(missing_images, prog_bar=True)
Пример #14
0
    def version(self, ext=False):
        """Returns the version of Orchest.

        Args:
            ext: If True return the extensive version of Orchest.
                Meaning that the version of every pulled image is
                checked.

        """
        if not ext:
            version = os.getenv("ORCHEST_VERSION")
            utils.echo(f"Orchest version: {version}")
            return

        utils.echo("Getting versions of all containers...")

        stdouts = self.resource_manager.containers_version()
        stdout_values = set()
        for img, stdout in stdouts.items():
            stdout_values.add(stdout)
            utils.echo(f"{img:<44}: {stdout}")

        # If not all versions are the same.
        if len(stdout_values) > 1:
            utils.echo(
                "Not all containers are running on the same version of Orchest, which"
                " can lead to the application crashing. You can fix this by running:",
            )
            utils.echo("\torchest update")
            utils.echo("To get all containers on the same version again.")
Пример #15
0
    def update(self, mode=None):
        """Update Orchest.

        Args:
            mode: The mode in which to update Orchest. This is either
                ``None`` or ``"web"``, where the latter is used when
                update is invoked through the update-server.

        """
        # Get all installed containers.
        pulled_images = self.resource_manager.get_images()

        # Pull images. It is possible to pull new image whilst the older
        # versions of those images are running.
        # TODO: remove the warning from the orchest.sh script that
        #       containers will be shut down.
        utils.echo("Updating...")

        _, running_containers = self.resource_manager.get_containers(
            state="running")
        if self.is_running(running_containers):
            utils.echo("Using Orchest whilst updating is NOT recommended.")

        # Update the Orchest git repo to get the latest changes to the
        # "userdir/" structure.
        exit_code = utils.update_git_repo()
        if exit_code != 0:
            utils.echo("Cancelling update...")
            utils.echo(
                "It seems like you have unstaged changes in the 'orchest'"
                " repository. Please commit or stash them as 'orchest update'"
                " pulls the newest changes to the 'userdir/' using a rebase.",
            )
            logger.error("Failed update due to unstaged changes.")
            return

        logger.info("Updating images:\n" + "\n".join(pulled_images))
        self.docker_client.pull_images(pulled_images,
                                       prog_bar=True,
                                       force=True)

        # Delete user-built environment images to avoid the issue of
        # having environments with mismatching Orchest SDK versions.
        logger.info("Deleting user-built environment images.")
        self.resource_manager.remove_env_build_imgs()

        # Delete user-built Jupyter image to make sure the Jupyter
        # server is updated to the latest version of Orchest.
        logger.info("Deleting user-built Jupyter image.")
        self.resource_manager.remove_jupyter_build_imgs()

        # We invoke the Orchest restart from the webserver ui-updater.
        # Hence we do not show the message to restart manually.
        if mode == "web":
            utils.echo("Update completed.")
        else:
            # Let the user know they need to restart the application
            # for the changes to take effect. NOTE: otherwise Orchest
            # might also be running a mix of containers on different
            # versions.
            utils.echo(
                "Don't forget to restart Orchest for the changes to take effect:"
            )
            utils.echo("\torchest restart")
Пример #16
0
    def update(self, mode=None, dev: bool = False):
        """Update Orchest.

        Args:
            mode: The mode in which to update Orchest. This is either
                ``None`` or ``"web"``, where the latter is used when
                update is invoked through the update-server.

        """
        utils.echo("Updating...")

        _, running_containers = self.resource_manager.get_containers(state="running")

        if utils.is_orchest_running(running_containers):
            if mode != "web":
                # In the web updater it is not possible to cancel the
                # update once started. So there is no value in showing
                # this message or sleeping.
                utils.echo(
                    "Using Orchest whilst updating is NOT supported and will be shut"
                    " down, killing all active pipeline runs and session. You have 2s"
                    " to cancel the update operation."
                )

                # Give the user the option to cancel the update
                # operation using a keyboard interrupt.
                time.sleep(2)

            skip_containers = []
            if mode == "web":
                # It is possible to pull new images whilst the older
                # versions of those images are running. We will invoke
                # Orchest restart from the webserver ui-updater.
                skip_containers = [
                    "orchest/update-server:latest",
                    "orchest/auth-server:latest",
                    "orchest/nginx-proxy:latest",
                    "postgres:13.1",
                ]

            self.stop(skip_containers=skip_containers)

        # Update the Orchest git repo to get the latest changes to the
        # "userdir/" structure.
        if not dev:
            exit_code = utils.update_git_repo()
            if exit_code == 0:
                logger.info("Successfully updated git repo during update.")
            elif exit_code == 21:
                utils.echo("Cancelling update...")
                utils.echo(
                    "Make sure you have the master branch checked out before updating."
                )
                logger.error(
                    "Failed update due to master branch not being checked out."
                )
                return
            else:
                utils.echo("Cancelling update...")
                utils.echo(
                    "It seems like you have unstaged changes in the 'orchest'"
                    " repository. Please commit or stash them as 'orchest update'"
                    " pulls the newest changes to the 'userdir/' using a rebase.",
                )
                logger.error("Failed update due to unstaged changes.")
                return

        # Get all installed images and pull new versions. The pulled
        # images are checked to make sure optional images, e.g. lang
        # specific images, are updated as well.
        pulled_images = self.resource_manager.get_images()
        to_pull_images = set(ORCHEST_IMAGES["minimal"]) | set(pulled_images)
        logger.info("Updating images:\n" + "\n".join(to_pull_images))
        self.docker_client.pull_images(to_pull_images, prog_bar=True, force=True)

        # Delete user-built environment images to avoid the issue of
        # having environments with mismatching Orchest SDK versions.
        logger.info("Deleting user-built environment images.")
        self.resource_manager.remove_env_build_imgs()

        # Delete user-built Jupyter image to make sure the Jupyter
        # server is updated to the latest version of Orchest.
        logger.info("Deleting user-built Jupyter image.")
        self.resource_manager.remove_jupyter_build_imgs()

        # Delete Orchest dangling images.
        self.resource_manager.remove_orchest_dangling_imgs()

        if mode == "web":
            utils.echo("Update completed.")
        else:
            utils.echo("Update completed. To start Orchest again, run:")
            utils.echo("\torchest start")

        utils.echo(
            "Checking whether all containers are running the same version of Orchest."
        )
        self.version(ext=True)
Пример #17
0
    def version(self, ext=False):
        """Returns the version of Orchest.

        Args:
            ext: If True return the extensive version of Orchest.
                Meaning that the version of every pulled image is
                checked.

        """
        if not ext:
            version = os.getenv("ORCHEST_VERSION")
            utils.echo(f"Orchest version: {version}")
            return

        utils.echo("Getting versions of all containers...")
        pulled_images = self.resource_manager.get_images(orchest_owned=True)

        configs = {}
        for img in pulled_images:
            configs[img] = {
                "Image": img,
                "Entrypoint": ["printenv", "ORCHEST_VERSION"],
            }

        stdouts = self.docker_client.run_containers(configs, detach=False)
        stdout_values = set()
        for img, info in stdouts.items():
            stdout = info["stdout"]
            # stdout = ['v0.4.1-58-g3f4bc64\n']
            stdout = stdout[0].rstrip()
            stdout_values.add(stdout)
            utils.echo(f"{img:<44}: {stdout}")

        # If not all versions are the same.
        if len(stdout_values) > 1:
            utils.echo(
                "Not all containers are running on the same version of Orchest, which"
                " can lead to the application crashing. You can fix this by running:",
                wrap=WRAP_LINES,
            )
            utils.echo("\torchest update")
            utils.echo("To get all containers on the same version again.")
Пример #18
0
    def update(self, mode=None, dev: bool = False):
        """Update Orchest.

        Args:
            mode: The mode in which to update Orchest. This is either
                ``None`` or ``"web"``, where the latter is used when
                update is invoked through the update-server.

        """
        utils.echo("Updating...")

        _, running_containers = self.resource_manager.get_containers(
            state="running")
        if utils.is_orchest_running(running_containers):
            utils.echo(
                "Using Orchest whilst updating is NOT supported and will be shut"
                " down, killing all active pipeline runs and session. You have 2s"
                " to cancel the update operation.")

            # Give the user the option to cancel the update operation
            # using a keyboard interrupt.
            time.sleep(2)

            # It is possible to pull new image whilst the older versions
            # of those images are running.
            self.stop(skip_containers=[
                "orchest/update-server:latest",
                "orchest/auth-server:latest",
                "orchest/nginx-proxy:latest",
                "postgres:13.1",
            ])

        # Update the Orchest git repo to get the latest changes to the
        # "userdir/" structure.
        if not dev:
            exit_code = utils.update_git_repo()
            if exit_code != 0:
                utils.echo("Cancelling update...")
                utils.echo(
                    "It seems like you have unstaged changes in the 'orchest'"
                    " repository. Please commit or stash them as 'orchest update'"
                    " pulls the newest changes to the 'userdir/' using a rebase.",
                )
                logger.error("Failed update due to unstaged changes.")
                return

        # Get all installed images and pull new versions. The pulled
        # images are checked to make sure optional images, e.g. lang
        # specific images, are updated as well.
        pulled_images = self.resource_manager.get_images()
        to_pull_images = set(ORCHEST_IMAGES["minimal"]) | set(pulled_images)
        logger.info("Updating images:\n" + "\n".join(to_pull_images))
        self.docker_client.pull_images(to_pull_images,
                                       prog_bar=True,
                                       force=True)

        # Delete user-built environment images to avoid the issue of
        # having environments with mismatching Orchest SDK versions.
        logger.info("Deleting user-built environment images.")
        self.resource_manager.remove_env_build_imgs()

        # Delete user-built Jupyter image to make sure the Jupyter
        # server is updated to the latest version of Orchest.
        logger.info("Deleting user-built Jupyter image.")
        self.resource_manager.remove_jupyter_build_imgs()

        # Delete Orchest dangling images.
        self.resource_manager.remove_orchest_dangling_imgs()

        # We invoke the Orchest restart from the webserver ui-updater.
        # Hence we do not show the message to restart manually.
        if mode == "web":
            utils.echo("Update completed.")
        else:
            # Let the user know they need to restart the application
            # for the changes to take effect. NOTE: otherwise Orchest
            # might also be running a mix of containers on different
            # versions.
            utils.echo(
                "Don't forget to restart Orchest for the changes to take effect:"
            )
            utils.echo("\torchest restart")