diff --git a/.dockerignore b/.dockerignore
index f673a52ccf407..133e41420b7b4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -28,7 +28,6 @@
 
 # Allow only these directories
 !airflow
-!common
 !dags
 !dev
 !docs
@@ -39,6 +38,7 @@
 !.coveragerc
 !.rat-excludes
 !.flake8
+!.dockerignore
 !pylintrc
 !LICENSE
 !MANIFEST.in
@@ -60,8 +60,8 @@
 !setup.cfg
 !setup.py
 
-# Entrypoint script
-!scripts/docker/entrypoint.sh
+# docker scripts
+!scripts/docker
 
 # Now - ignore unnecessary files inside allowed directories
 # This goes after the allowed directories
@@ -72,14 +72,13 @@ airflow/git_version
 # Exclude static www files generated by NPM
 airflow/www/static/coverage
 airflow/www/static/dist
+airflow/www/static/docs
 airflow/www/node_modules
 # Exclude static www_rbac files generated by NPM in v1-10-test
 airflow/www_rbac/static/coverage
 airflow/www_rbac/static/dist
 airflow/www_rbac/node_modules
-
-# Exclude link to docs
-airflow/www/static/docs
+airflow/www_rbac/static/docs
 
 # Exclude python generated files
 **/__pycache__/
diff --git a/.gitignore b/.gitignore
index 04b080904bb14..0ecd95701cdd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ unittests.db
 airflow/git_version
 airflow/www/static/coverage/
 airflow/www/static/dist
+airflow/www/docs
 airflow/www_rbac/static/coverage/
 airflow/www_rbac/static/dist/
 
@@ -142,8 +143,8 @@ rat-results.txt
 # Kubernetes generated templated files
 *.generated
 *.tar.gz
-scripts/ci/kubernetes/kube/.generated/airflow.yaml
-scripts/ci/kubernetes/docker/requirements.txt
+scripts/ci/in_container/kubernetes/kube/.generated/airflow.yaml
+scripts/ci/in_container/kubernetes/docker/requirements.txt
 
 # Node & Webpack Stuff
 *.entry.js
@@ -171,5 +172,6 @@ dmypy.json
 /hive_scratch_dir/
 /.bash_aliases
 /.bash_history
+/.kube
 /.inputrc
 log.txt*
diff --git a/.travis.yml b/.travis.yml
index d2b1dba70e567..541cfc753216a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -22,9 +22,10 @@ env:
   global:
     - BUILD_ID=${TRAVIS_BUILD_ID}
     - MOUNT_LOCAL_SOURCES="false"
-    - AIRFLOW_MOUNT_HOST_VOLUMES_FOR_STATIC_CHECKS="false"
+    - MOUNT_HOST_VOLUMES="false"
     - FORCE_ANSWER_TO_QUESTIONS="yes"
     - AIRFLOW_CI_SILENT="true"
+    - DOCKERHUB_USER="potiuk" # Temporary to make it builds using images from test dockerhub before merging
     - CI="true"
 python: "3.6"
 stages:
@@ -48,39 +49,38 @@ jobs:
     - name: "Tests postgres kubernetes python 3.6 (persistent)"
       env: >-
         BACKEND=postgres
-        ENV=kubernetes
-        KUBERNETES_VERSION=v1.15.0
-        KUBERNETES_MODE=persistent_mode
         PYTHON_VERSION=3.6
+        START_KUBERNETES_CLUSTER=true
+        KUBERNETES_MODE=persistent_mode
+        KUBERNETES_VERSION=v1.15.3
       python: "3.6"
       stage: test
+      script: travis_wait 30 "./scripts/ci/ci_run_airflow_testing.sh" tests.integration.kubernetes
     - name: "Tests postgres kubernetes python 3.6 (git)"
       env: >-
         BACKEND=postgres
-        ENV=kubernetes
-        KUBERNETES_VERSION=v1.15.0
-        KUBERNETES_MODE=git_mode
         PYTHON_VERSION=3.6
+        START_KUBERNETES_CLUSTER=true
+        KUBERNETES_MODE=git_mode
+        KUBERNETES_VERSION=v1.15.3
       python: "3.6"
       stage: test
+      script: travis_wait 30 "./scripts/ci/ci_run_airflow_testing.sh" tests.integration.kubernetes
     - name: "Tests postgres python 3.6"
       env: >-
         BACKEND=postgres
-        ENV=docker
         PYTHON_VERSION=3.6
       python: "3.6"
       stage: test
     - name: "Tests sqlite python 3.6"
       env:
         BACKEND=sqlite
-        ENV=docker
         PYTHON_VERSION=3.6
       python: "3.6"
       stage: test
     - name: "Tests mysql python 3.7"
       env:
         BACKEND=mysql
-        ENV=docker
         PYTHON_VERSION=3.7
       python: "3.7"
       stage: test
@@ -88,4 +88,4 @@ services:
   - docker
 before_install:
   - ./scripts/ci/ci_before_install.sh
-script: travis_wait 30 "./scripts/ci/ci_run_airflow_testing.sh"
+script: "./scripts/ci/ci_run_airflow_testing.sh"
diff --git a/BREEZE.rst b/BREEZE.rst
index a6b4818e08b35..e33a815265a1a 100644
--- a/BREEZE.rst
+++ b/BREEZE.rst
@@ -79,11 +79,11 @@ images maintained on the Docker Hub in the ``apache/airflow`` repository.
 
 There are three images that we are currently managing:
 
-* **CI image*** that is used for testing od both Unit tests and static check tests.
-  It contains a lot test-related packages (size of ~1GB).
-  Its tag follows the pattern of ``<BRANCH>-python<PYTHON_VERSION>-ci``
+* **CI image*** that is used for testing. It contains a lot more test-related installed software
+  (size of ~1GB). Its tag follows the pattern of ``<BRANCH>-python<PYTHON_VERSION>-ci``
   (for example, ``apache/airflow:master-python3.6-ci``). The image is built using the
   `<Dockerfile>`_ Dockerfile.
+* **Production-ready image** that is used TODO
 
 Before you run tests, enter the environment or run local static checks, the necessary local images should be
 pulled and built from Docker Hub. This happens automatically for the test environment but you need to
@@ -224,7 +224,7 @@ follows:
 
   .. code-block:: bash
 
-    ./breeze --python 3.6 --backend mysql --env docker
+    ./breeze --python 3.6 --backend mysql
 
 The choices you make are persisted in the ``./.build/`` cache directory so that next time when you use the
 ``breeze`` script, it could use the values that were used previously. This way you do not have to specify
@@ -233,25 +233,6 @@ default settings.
 
 The defaults when you run the Breeze environment are Python 3.6, Sqlite, and Docker.
 
-Available Docker Environments
-..............................
-
-You can choose a container environment when you run Breeze with ``--env`` flag.
-Running the default ``docker`` environment takes a considerable amount of resources. You can run a
-slimmed-down version of the environment - just the Apache Airflow container - by choosing ``bare``
-environment instead.
-
-The following environments are available:
-
- * The ``docker`` environment (default): starts all dependencies required by a full integration test suite
-   (Postgres, Mysql, Celery, etc). This option is resource intensive so do not forget to
-   [stop environment](#stopping-the-environment) when you are finished. This option is also RAM intensive
-   and can slow down your machine.
- * The ``kubernetes`` environment: Runs Airflow tests within a Kubernetes cluster.
- * The ``bare`` environment:  runs Airflow in the Docker without any external dependencies.
-   It only works for independent tests. You can only run it with the sqlite backend.
-
-
 Cleaning Up the Environment
 ---------------------------
 
@@ -535,6 +516,7 @@ the database.
 
    run-tests --with-db-init tests.core:TestCore.test_check_operators -- -s --logging-level=DEBUG
 
+
 Running Tests for a Specified Target
 ------------------------------------
 
@@ -557,6 +539,17 @@ You can also specify individual tests or a group of tests:
 
     ./breeze --test-target tests.core:TestCore
 
+Running Tests with Kubernetes Cluster
+-------------------------------------
+
+If you want to run Kubernetes tests, you need to set use ``--start-kubernetes-cluster`` Breeze flag.
+It will create Kubernetes cluster (using `kind (Kubernetes-IN-Docker) <https://kind.sigs.k8s.io/>`_.)
+right after entering the container.
+This might take some time. Note that in order to use latest sources you need to
+rebuild the latest docker image for Breeze first.
+
+All kubernetes tests are in ``tests.integration.kubernetes`` package.
+
 Running Static Code Checks
 --------------------------
 
@@ -717,33 +710,43 @@ On the host:
   ./scripts/ci/ci_pylint.sh ./airflow/example_dags/test_utils.py
 
 Running Test Suites via Scripts
---------------------------------------------
+-------------------------------
+
+To run tests use ``./scripts/ci/local_ci_run_airflow_testing.sh`` scripts.
+
+Optionally use one of the variables (defaults are bold):
+
+* PYTHON_VERSION is one of **3.6**/3.7
+* BACKEND is one of **sqlite**/postgres/mysql
 
-To run all tests with default settings (Python 3.6, Sqlite backend, "docker" environment), enter:
+For Kubernetes tests you should set ``START_KUBERNETES_CLUSTER`` variable to true.
+This will start Kubernetes cluster inside the container
+using `kind (Kubernetes-IN-Docker) <https://kind.sigs.k8s.io/>`_.
+You might also optionally specify one of the variables (defaults are bold):
+
+* KUBERNETES_MODE is a mode of kubernetes: one of persistent_mode/**git_mode**
+* KUBERNETES_VERSION is required for Kubernetes tests. Default: v1.15.3.
+
+To run all non-kubernetes tests with default settings (Python 3.6, Sqlite backend, no kubernetes), enter:
 
 .. code-block::
 
   ./scripts/ci/local_ci_run_airflow_testing.sh
 
 
-To select Python 3.6 version, Postgres backend, and a "docker" environment, specify:
+To select Python 3.6 version, Postgres backend, specify:
 
 .. code-block::
 
-  PYTHON_VERSION=3.6 BACKEND=postgres ENV=docker ./scripts/ci/local_ci_run_airflow_testing.sh
+  PYTHON_VERSION=3.6 BACKEND=postgres ./scripts/ci/local_ci_run_airflow_testing.sh
 
 To run Kubernetes tests, enter:
 
 .. code-block::
 
-  KUBERNETES_VERSION==v1.13.0 KUBERNETES_MODE=persistent_mode BACKEND=postgres ENV=kubernetes \
+  START_KUBERNETES_CLUSTER=true KUBERNETES_MODE=persistent_mode KUBERNETES_VERSION=v1.15.3 BACKEND=postgres \
     ./scripts/ci/local_ci_run_airflow_testing.sh
 
-* PYTHON_VERSION is one of 3.6/3.7
-* BACKEND is one of postgres/sqlite/mysql
-* ENV is one of docker/kubernetes/bare
-* KUBERNETES_VERSION is required for Kubernetes tests. Currently, it is KUBERNETES_VERSION=v1.13.0.
-* KUBERNETES_MODE is a mode of kubernetes: either persistent_mode or git_mode.
 
 Using Your Host IDE with Breeze
 ===============================
@@ -837,7 +840,6 @@ This is the current syntax for  `./breeze <./breeze>`_:
 .. code-block:: text
 
 
-
   Usage: breeze [FLAGS] \
     [-k]|[-S <STATIC_CHECK>]|[-F <STATIC_CHECK>]|[-O]|[-e]|[-a]|[-b]|[-t <TARGET>]|[-x <COMMAND>]|[-d <COMMAND>] \
     -- <EXTRA_ARGS>
@@ -851,9 +853,9 @@ This is the current syntax for  `./breeze <./breeze>`_:
     * Build documentation with -O, --build-docs command
     * Setup local virtualenv with -e, --setup-virtualenv command
     * Setup autocomplete for itself with -a, --setup-autocomplete command
-    * Build docker image with -b, --build-only command
+    * Build CI docker images with -b, --build-only command
     * Run test target specified with -t, --test-target connad
-    * Execute arbitrary command in the test environmenrt with -x, --execute-command command
+    * Execute arbitrary command in the test environment with -x, --execute-command command
     * Execute arbitrary docker-compose command with -d, --docker-compose command
 
   ** Commands
@@ -871,8 +873,13 @@ This is the current syntax for  `./breeze <./breeze>`_:
 
   -S, --static-check <STATIC_CHECK>
           Run selected static checks for currently changed files. You should specify static check that
-          you would like to run or 'all' to run all checks. One of
-          [ all all-but-pylint check-hooks-apply check-merge-conflict check-executables-have-shebangs check-xml detect-private-key doctoc end-of-file-fixer flake8 forbid-tabs insert-license check-apache-license lint-dockerfile mixed-line-ending mypy pylint shellcheck].
+          you would like to run or 'all' to run all checks. One of:
+
+  all all-but-pylint build check-apache-license check-executables-have-shebangs check-hooks-apply
+  check-merge-conflict check-xml detect-private-key doctoc end-of-file-fixer flake8 forbid-tabs
+  insert-license isort lint-dockerfile mixed-line-ending mypy pylint python-no-log-warn rst-backticks
+  shellcheck yamllint
+
           You can pass extra arguments including options to to the pre-commit framework as
           <EXTRA_ARGS> passed after --. For example:
 
@@ -885,8 +892,13 @@ This is the current syntax for  `./breeze <./breeze>`_:
 
   -F, --static-check-all-files <STATIC_CHECK>
           Run selected static checks for all applicable files. You should specify static check that
-          you would like to run or 'all' to run all checks. One of
-          [ all all-but-pylint check-hooks-apply check-merge-conflict check-executables-have-shebangs check-xml detect-private-key doctoc end-of-file-fixer flake8 forbid-tabs insert-license check-apache-license lint-dockerfile mixed-line-ending mypy pylint shellcheck].
+          you would like to run or 'all' to run all checks. One of:
+
+  all all-but-pylint build check-apache-license check-executables-have-shebangs check-hooks-apply
+  check-merge-conflict check-xml detect-private-key doctoc end-of-file-fixer flake8 forbid-tabs
+  insert-license isort lint-dockerfile mixed-line-ending mypy pylint python-no-log-warn rst-backticks
+  shellcheck yamllint
+
           You can pass extra arguments including options to the pre-commit framework as
           <EXTRA_ARGS> passed after --. For example:
 
@@ -908,7 +920,7 @@ This is the current syntax for  `./breeze <./breeze>`_:
           shell and when typing breeze command <TAB> will provide autocomplete for parameters and values.
 
   -b, --build-only
-          Only build docker images but do not enter the airflow-testing docker container.
+          Only build CI docker images but do not enter the airflow-testing docker container.
 
   -t, --test-target <TARGET>
           Run the specified unit test target. There might be multiple
@@ -942,22 +954,29 @@ This is the current syntax for  `./breeze <./breeze>`_:
           Python version used for the image. This is always major/minor version.
           One of [ 3.6 3.7 ]. Default is the python3 or python on the path.
 
-  -E, --env <ENVIRONMENT>
-          Environment to use for tests. It determines which types of tests can be run.
-          One of [ docker kubernetes ]. Default: docker
-
   -B, --backend <BACKEND>
           Backend to use for tests - it determines which database is used.
           One of [ sqlite mysql postgres ]. Default: sqlite
 
-  -K, --kubernetes-version <KUBERNETES_VERSION>
-          Kubernetes version - only used in case of 'kubernetes' environment.
-          One of [ v1.13.0 ]. Default: v1.13.0
+  -K, --start-kubernetes-cluster
+          Starts test kubernetes cluster after entering the environment. The cluster is started using
+          Kubernetes Mode selected and Kubernetes version specifed via --kubernetes-mode and
+          --kubernetes-version flags.
+
+  -Z, --recreate-kubernetes-cluster
+          Recreates kubernetes cluster if one has already been created. By default if you do not stop
+          environment, the kubernetes cluster created for testing is continuously running and when
+          you start kubernetes testing again it will be reused. You can force deletion and recreation
+          of such cluster with this flag.
 
   -M, --kubernetes-mode <KUBERNETES_MODE>
-          Kubernetes mode - only used in case of 'kubernetes' environment.
+          Kubernetes mode - only used in case --start-kubernetes-cluster flag is specified.
           One of [ persistent_mode git_mode ]. Default: git_mode
 
+  -V, --kubernetes-version <KUBERNETES_VERSION>
+          Kubernetes version - only used in case --start-kubernetes-cluster flag is specified.
+          One of [ v1.15.3 v1.16.2 ]. Default: v1.15.3
+
   -s, --skip-mounting-source-volume
           Skips mounting local volume with sources - you get exactly what is in the
           docker image rather than your current local sources of airflow.
@@ -971,6 +990,9 @@ This is the current syntax for  `./breeze <./breeze>`_:
   -n, --assume-no
           Assume 'no' answer to all questions.
 
+  -q, --assume-quit
+          Assume 'quit' answer to all questions.
+
   -C, --toggle-suppress-cheatsheet
           Toggles on/off cheatsheet displayed before starting bash shell
 
@@ -990,15 +1012,15 @@ This is the current syntax for  `./breeze <./breeze>`_:
           automatically for the first time or when changes are detected in
           package-related files, but you can force it using this flag.
 
-  -R, --force-build-images-clean
-          Force build images without cache. This will remove the pulled or build images
-          and start building images from scratch. This might take a long time.
-
   -p, --force-pull-images
           Forces pulling of images from DockerHub before building to populate cache. The
           images are pulled by default only for the first time you run the
           environment, later the locally build images are used as cache.
 
+  -L, --use-local-cache
+          Uses local cache to build images. No pulled images will be used, but results of local builds in
+          the Docker cache are used instead.
+
   -u, --push-images
           After building - uploads the images to DockerHub
           It is useful in case you use your own DockerHub user to store images and you want
@@ -1011,7 +1033,6 @@ This is the current syntax for  `./breeze <./breeze>`_:
 
  .. END BREEZE HELP MARKER
 
-
 Convenience Scripts
 -------------------
 
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 33aa1161b2a7d..da1e5223903fa 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -137,7 +137,7 @@ these guidelines:
     of the same PR. Doc string is often sufficient. Make sure to follow the
     Sphinx compatible standards.
 
--   Make sure the pull request works for Python 3.5, 3.6 and 3.7.
+-   Make sure the pull request works for Python 3.6 and 3.7.
 
 -   Run tests locally before opening PR.
 
diff --git a/Dockerfile b/Dockerfile
index 390433574c51c..23bd38c636bb0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,12 +15,18 @@
 #
 # WARNING: THIS DOCKERFILE IS NOT INTENDED FOR PRODUCTION USE OR DEPLOYMENT.
 #
-ARG PYTHON_BASE_IMAGE="python:3.6-slim-stretch"
-FROM ${PYTHON_BASE_IMAGE} as main
+# Base image for the whole Docker file
+ARG PYTHON_BASE_IMAGE="python:3.6-slim-buster"
+ARG NODE_BASE_IMAGE="node:12.11.1-buster"
+
+############################################################################################################
+# Base image for Airflow - contains dependencies used by the main CI image
+############################################################################################################
+FROM ${PYTHON_BASE_IMAGE} as airflow-base
 
 SHELL ["/bin/bash", "-o", "pipefail", "-e", "-u", "-x", "-c"]
 
-ARG PYTHON_BASE_IMAGE="python:3.6-slim-stretch"
+ARG PYTHON_BASE_IMAGE="python:3.6-slim-buster"
 ENV PYTHON_BASE_IMAGE=${PYTHON_BASE_IMAGE}
 
 ARG AIRFLOW_VERSION="2.0.0.dev0"
@@ -35,22 +41,22 @@ ENV DEBIAN_FRONTEND=noninteractive LANGUAGE=C.UTF-8 LANG=C.UTF-8 LC_ALL=C.UTF-8
     LC_CTYPE=C.UTF-8 LC_MESSAGES=C.UTF-8
 
 # By increasing this number we can do force build of all dependencies
-ARG DEPENDENCIES_EPOCH_NUMBER="2"
-# Increase the value below to force renstalling of all dependencies
-ENV DEPENDENCIES_EPOCH_NUMBER=${DEPENDENCIES_EPOCH_NUMBER}
+# It can also be overwritten manually by setting the build variable.
+ARG APT_DEPENDENCIES_EPOCH_NUMBER="1"
+ENV APT_DEPENDENCIES_EPOCH_NUMBER=${APT_DEPENDENCIES_EPOCH_NUMBER}
 
-# Install curl and gnupg2 - needed to download nodejs in the next step
-RUN apt-get update \
-    && apt-get install -y --no-install-recommends \
-           curl \
-           gnupg2 \
-    && apt-get autoremove -yqq --purge \
-    && apt-get clean \
-    && rm -rf /var/lib/apt/lists/*
+# PIP version used to install dependencies
+ARG PIP_VERSION="19.0.2"
+ENV PIP_VERSION=${PIP_VERSION}
+
+RUN pip install --upgrade pip==${PIP_VERSION}
+
+# By default PIP install run without cache to make image smaller
+ARG PIP_NO_CACHE_DIR="true"
+ENV PIP_NO_CACHE_DIR=${PIP_NO_CACHE_DIR}
 
 # Install basic apt dependencies
-RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
-    && apt-get update \
+RUN apt-get update \
     && apt-get install -y --no-install-recommends \
            apt-utils \
            build-essential \
@@ -69,68 +75,57 @@ RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
            libssl-dev \
            locales  \
            netcat \
-           nodejs \
            rsync \
            sasl2-bin \
            sudo \
+           libmariadb-dev-compat \
     && apt-get autoremove -yqq --purge \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/*
 
-# Install graphviz - needed to build docs with diagrams
-RUN apt-get update \
-    && apt-get install -y --no-install-recommends \
-           graphviz \
-    && apt-get autoremove -yqq --purge \
-    && apt-get clean \
-    && rm -rf /var/lib/apt/lists/*
-
-# Install MySQL client from Oracle repositories (Debian installs mariadb)
-RUN KEY="A4A9406876FCBD3C456770C88C718D3B5072E1F5" \
-    && GNUPGHOME="$(mktemp -d)" \
-    && export GNUPGHOME \
-    && for KEYSERVER in $(shuf -e \
-            ha.pool.sks-keyservers.net \
-            hkp://p80.pool.sks-keyservers.net:80 \
-            keyserver.ubuntu.com \
-            hkp://keyserver.ubuntu.com:80 \
-            pgp.mit.edu) ; do \
-          gpg --keyserver "${KEYSERVER}" --recv-keys "${KEY}" && break || true ; \
-       done \
-    && gpg --export "${KEY}" | apt-key add - \
-    && gpgconf --kill all \
-    rm -rf "${GNUPGHOME}"; \
-    apt-key list > /dev/null \
-    && echo "deb http://repo.mysql.com/apt/debian/ stretch mysql-5.6" | tee -a /etc/apt/sources.list.d/mysql.list \
-    && apt-get update \
-    && apt-get install --no-install-recommends -y \
-        libmysqlclient-dev \
-        mysql-client \
-    && apt-get autoremove -yqq --purge \
-    && apt-get clean && rm -rf /var/lib/apt/lists/*
-
 RUN adduser airflow \
     && echo "airflow ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/airflow \
     && chmod 0440 /etc/sudoers.d/airflow
 
-ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
+############################################################################################################
+# CI airflow image
+############################################################################################################
+FROM airflow-base as airflow-ci
+
+SHELL ["/bin/bash", "-o", "pipefail", "-e", "-u", "-x", "-c"]
+
+# Setting to 1 speeds up building the image. Cassandra driver without CYTHON saves around 10 minutes
+# But might not be suitable for production image
+ENV CASS_DRIVER_NO_CYTHON="1"
+ENV CASS_DRIVER_BUILD_CONCURRENCY=8
+
+ENV JAVA_HOME=/usr/lib/jvm/adoptopenjdk-8-hotspot-amd64/
+
+# By changing the CI build epoch we can force reinstalling apt dependenecies for CI
+# It can also be overwritten manually by setting the build variable.
+ARG CI_APT_DEPENDENCIES_EPOCH_NUMBER="1"
+ENV CI_APT_DEPENDENCIES_EPOCH_NUMBER=${CI_APT_DEPENDENCIES_EPOCH_NUMBER}
 
-# Note missing man directories on debian-stretch
-# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
 RUN mkdir -pv /usr/share/man/man1 \
     && mkdir -pv /usr/share/man/man7 \
     && apt-get update \
+    && apt-get install --no-install-recommends -y \
+         apt-transport-https ca-certificates wget dirmngr gnupg software-properties-common curl gnupg2 \
+    && export APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 \
+    && curl -sL https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - \
+    && curl -sL https://deb.nodesource.com/setup_10.x | bash - \
+    && add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ \
+    && apt-get update \
     && apt-get install --no-install-recommends -y \
       gnupg \
-      apt-transport-https \
-      ca-certificates \
-      software-properties-common \
+      graphviz \
       krb5-user \
       ldap-utils \
       less \
       lsb-release \
+      nodejs \
       net-tools \
-      openjdk-8-jdk \
+      adoptopenjdk-8-hotspot \
       openssh-client \
       openssh-server \
       postgresql-client \
@@ -141,7 +136,8 @@ RUN mkdir -pv /usr/share/man/man1 \
       vim \
     && apt-get autoremove -yqq --purge \
     && apt-get clean \
-    && rm -rf /var/lib/apt/lists/*
+    && rm -rf /var/lib/apt/lists/* \
+    ;
 
 ENV HADOOP_DISTRO="cdh" HADOOP_MAJOR="5" HADOOP_DISTRO_VERSION="5.11.0" HADOOP_VERSION="2.6.0" \
     HADOOP_HOME="/opt/hadoop-cdh"
@@ -188,7 +184,7 @@ RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \
     && apt-get autoremove -yqq --purge \
     && apt-get clean && rm -rf /var/lib/apt/lists/*
 
-ARG KUBECTL_VERSION="v1.15.0"
+ARG KUBECTL_VERSION="v1.15.3"
 ENV KUBECTL_VERSION=${KUBECTL_VERSION}
 ARG KIND_VERSION="v0.5.0"
 ENV KIND_VERSION=${KIND_VERSION}
@@ -232,10 +228,6 @@ RUN mkdir -pv ${AIRFLOW_HOME} \
     mkdir -pv ${AIRFLOW_HOME}/dags \
     mkdir -pv ${AIRFLOW_HOME}/logs
 
-# Increase the value here to force reinstalling Apache Airflow pip dependencies
-ARG PIP_DEPENDENCIES_EPOCH_NUMBER="1"
-ENV PIP_DEPENDENCIES_EPOCH_NUMBER=${PIP_DEPENDENCIES_EPOCH_NUMBER}
-
 # Optimizing installation of Cassandra driver
 # Speeds up building the image - cassandra driver without CYTHON saves around 10 minutes
 ARG CASS_DRIVER_NO_CYTHON="1"
@@ -263,77 +255,44 @@ ENV AIRFLOW_REPO=${AIRFLOW_REPO}
 ARG AIRFLOW_BRANCH=master
 ENV AIRFLOW_BRANCH=${AIRFLOW_BRANCH}
 
-ENV AIRFLOW_GITHUB_DOWNLOAD=https://raw.githubusercontent.com/${AIRFLOW_REPO}/${AIRFLOW_BRANCH}
-
 # Airflow Extras installed
-ARG AIRFLOW_EXTRAS="all"
-ENV AIRFLOW_EXTRAS=${AIRFLOW_EXTRAS}
-
-RUN echo "Installing with extras: ${AIRFLOW_EXTRAS}."
-
-ARG AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD="false"
-ENV AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD=${AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD}
-
-# By changing the CI build epoch we can force reinstalling Arflow from the current master
-# It can also be overwritten manually by setting the AIRFLOW_CI_BUILD_EPOCH environment variable.
-ARG AIRFLOW_CI_BUILD_EPOCH="1"
-ENV AIRFLOW_CI_BUILD_EPOCH=${AIRFLOW_CI_BUILD_EPOCH}
-
-# In case of CI-optimised builds we want to pre-install master version of airflow dependencies so that
-# We do not have to always reinstall it from the scratch.
-# This can be reinstalled from latest master by increasing PIP_DEPENDENCIES_EPOCH_NUMBER.
-# And is automatically reinstalled from the scratch every month
-RUN \
-    if [[ "${AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD}" == "true" ]]; then \
-        pip install \
-        "https://github.com/apache/airflow/archive/${AIRFLOW_BRANCH}.tar.gz#egg=apache-airflow[${AIRFLOW_EXTRAS}]" \
-        && pip uninstall --yes apache-airflow; \
-    fi
-
-# Install NPM dependencies here. The NPM dependencies don't change that often and we already have pip
-# installed dependencies in case of CI optimised build, so it is ok to install NPM deps here
-# Rather than after setup.py is added.
-COPY airflow/www/package-lock.json ${AIRFLOW_SOURCES}/airflow/www/package-lock.json
-COPY airflow/www/package.json ${AIRFLOW_SOURCES}/airflow/www/package.json
+ARG AIRFLOW_CI_EXTRAS="devel_ci"
+ENV AIRFLOW_CI_EXTRAS=${AIRFLOW_CI_EXTRAS}
 
-WORKDIR ${AIRFLOW_SOURCES}/airflow/www
+RUN echo "Installing with extras: ${AIRFLOW_CI_EXTRAS}."
 
-RUN npm ci
+ENV PATH="${HOME}/.local/bin:${PATH}"
 
-WORKDIR ${AIRFLOW_SOURCES}
-
-# Note! We are copying everything with airflow:airflow user:group even if we use root to run the scripts
-# This is fine as root user will be able to use those dirs anyway.
-
-# Airflow sources change frequently but dependency configuration won't change that often
-# We copy setup.py and other files needed to perform setup of dependencies
-# So in case setup.py changes we can install latest dependencies required.
-COPY setup.py ${AIRFLOW_SOURCES}/setup.py
-COPY setup.cfg ${AIRFLOW_SOURCES}/setup.cfg
+# Increase the value here to force reinstalling Apache Airflow pip dependencies
+ARG PIP_DEPENDENCIES_EPOCH_NUMBER="1"
+ENV PIP_DEPENDENCIES_EPOCH_NUMBER=${PIP_DEPENDENCIES_EPOCH_NUMBER}
 
-COPY airflow/version.py ${AIRFLOW_SOURCES}/airflow/version.py
-COPY airflow/__init__.py ${AIRFLOW_SOURCES}/airflow/__init__.py
-COPY airflow/bin/airflow ${AIRFLOW_SOURCES}/airflow/bin/airflow
+# In case of CI builds we want to pre-install master version of airflow dependencies so that
+# We do not have to always reinstall it from the scratch and loose time for that.
+# CI build is optimised for build speed
+RUN pip install --user \
+        "https://github.com/${AIRFLOW_REPO}/archive/${AIRFLOW_BRANCH}.tar.gz#egg=apache-airflow[${AIRFLOW_CI_EXTRAS}]" \
+        && pip uninstall --yes apache-airflow snakebite
 
-# The goal of this line is to install the dependencies from the most current setup.py from sources
-# This will be usually incremental small set of packages in CI optimized build, so it will be very fast
-# In non-CI optimized build this will install all dependencies before installing sources.
-RUN pip install -e ".[${AIRFLOW_EXTRAS}]"
+# Copy all www files here so that we can run npm building
+COPY airflow/www/ ${AIRFLOW_SOURCES}/airflow/www/
 
 WORKDIR ${AIRFLOW_SOURCES}/airflow/www
 
-# Copy all www files here so that we can run npm building for production
-COPY airflow/www/ ${AIRFLOW_SOURCES}/airflow/www/
+RUN mkdir -p "${AIRFLOW_SOURCES}/airflow/www/static" \
+    && mkdir -p "${AIRFLOW_SOURCES}/docs/build/_html" \
+    && pushd "${AIRFLOW_SOURCES}/airflow/www/static" || exit \
+    && ln -sf ../../../docs/_build/html docs \
+    && popd || exit
+
+RUN npm ci
 
-# Package NPM for production
 RUN npm run prod
 
 COPY ./scripts/docker/entrypoint.sh /entrypoint.sh
 
-COPY .bash_completion run-tests-complete run-tests ${HOME}/
-
-COPY .bash_completion.d/run-tests-complete \
-     ${HOME}/.bash_completion.d/run-tests-complete
+COPY .bash_completion run-tests-complete run-tests /root/
+COPY .bash_completion.d/run-tests-complete /root/.bash_completion.d/run-tests-complete
 
 RUN echo ". ${HOME}/.bash_completion" >> "${HOME}/.bashrc"
 
@@ -341,6 +300,23 @@ RUN chmod +x "${HOME}/run-tests-complete"
 
 RUN chmod +x "${HOME}/run-tests"
 
+WORKDIR ${AIRFLOW_SOURCES}
+
+# Airflow sources change frequently but dependency configuration won't change that often
+# We copy setup.py and other files needed to perform setup of dependencies
+# So in case setup.py changes we can install latest dependencies required.
+COPY setup.py ${AIRFLOW_SOURCES}/setup.py
+COPY setup.cfg ${AIRFLOW_SOURCES}/setup.cfg
+
+COPY airflow/version.py ${AIRFLOW_SOURCES}/airflow/version.py
+COPY airflow/__init__.py ${AIRFLOW_SOURCES}/airflow/__init__.py
+COPY airflow/bin/airflow ${AIRFLOW_SOURCES}/airflow/bin/airflow
+
+# The goal of this line is to install the dependencies from the most current setup.py from sources
+# This will be usually incremental small set of packages in the CI optimized build, so it will be very fast
+RUN pip install --user -e ".[${AIRFLOW_CI_EXTRAS}]" \
+    && pip uninstall --yes apache-airflow
+
 # Copy selected subdirectories only
 COPY .github/ ${AIRFLOW_SOURCES}/.github/
 COPY dags/ ${AIRFLOW_SOURCES}/dags/
@@ -355,21 +331,24 @@ COPY .coveragerc .rat-excludes .flake8 pylintrc LICENSE MANIFEST.in NOTICE CHANG
      setup.cfg setup.py \
      ${AIRFLOW_SOURCES}/
 
-WORKDIR ${AIRFLOW_SOURCES}
+# Reinstall airflow again - this time with sources and remove the sources after installation
+# It is not perfect because the sources are added as layer but it is still better
+RUN pip install --user -e ".[${AIRFLOW_CI_EXTRAS}]"
 
 # Additional python deps to install
 ARG ADDITIONAL_PYTHON_DEPS=""
 
 RUN if [[ -n "${ADDITIONAL_PYTHON_DEPS}" ]]; then \
-        pip install ${ADDITIONAL_PYTHON_DEPS}; \
+        pip install --user ${ADDITIONAL_PYTHON_DEPS}; \
     fi
 
+
 WORKDIR ${AIRFLOW_SOURCES}
 
 ENV PATH="${HOME}:${PATH}"
 
 EXPOSE 8080
 
-ENTRYPOINT ["/usr/local/bin/dumb-init", "--", "/entrypoint.sh"]
+ENTRYPOINT ["/root/.local/bin/dumb-init", "--", "/entrypoint.sh"]
 
 CMD ["--help"]
diff --git a/LOCAL_VIRTUALENV.rst b/LOCAL_VIRTUALENV.rst
index ac62091fb131a..63e9d4875fddb 100644
--- a/LOCAL_VIRTUALENV.rst
+++ b/LOCAL_VIRTUALENV.rst
@@ -23,11 +23,11 @@ Local Virtual Environment (virtualenv)
 
 Use the local virtualenv development option in the combination with the _`Breeze <BREEZE.rst#aout-airflow-breeze>`_
 development environment. This option helps you benefit from the infrastructure provided
-by your IDE (for example, IntelliJ's PyCharm/Idea) and work in the enviroment where all necessary dependencies and tests are 
+by your IDE (for example, IntelliJ's PyCharm/Idea) and work in the enviroment where all necessary dependencies and tests are
 available and set up within Docker images.
 
 But you can also use the local virtualenv as a standalone development option of
-you develop Airflow functionality that does not incur large external dependencies and 
+you develop Airflow functionality that does not incur large external dependencies and
 CI test coverage.
 
 These are examples of the development options available with the local virtualenv in your IDE:
@@ -46,10 +46,10 @@ Prerequisites
 Required Software Packages
 --------------------------
 
-Use system-level package managers like yum, apt-get for Linux, or 
+Use system-level package managers like yum, apt-get for Linux, or
 Homebrew for macOS to install required software packages:
 
-* Python (3.5 or 3.6)
+* Python (3.6 or 3.7)
 * MySQL
 * libxml
 
@@ -60,7 +60,7 @@ Extra Packages
 --------------
 
 You can also install extra packages (like ``[gcp]``, etc) via
-``pip install -e [EXTRA1,EXTRA2 ...]``. However, some of them may  
+``pip install -e [EXTRA1,EXTRA2 ...]``. However, some of them may
 have additional install and setup requirements for your local system.
 
 For example, if you have a trouble installing the mysql client on macOS and get
@@ -76,8 +76,8 @@ you should set LIBRARY\_PATH before running ``pip install``:
 
     export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl/lib/
 
-You are STRONGLY encouraged to also install and use `pre-commit hooks <CONTRIBUTING.rst#pre-commit-hooks>`_ 
-for your local virtualenv development environment. Pre-commit hooks can speed up your 
+You are STRONGLY encouraged to also install and use `pre-commit hooks <CONTRIBUTING.rst#pre-commit-hooks>`_
+for your local virtualenv development environment. Pre-commit hooks can speed up your
 development cycle a lot.
 
 The full list of extras is available in `<setup.py>`_.
@@ -85,11 +85,11 @@ The full list of extras is available in `<setup.py>`_.
 Creating a Local virtualenv
 ===========================
 
-To use your IDE for Airflow development and testing, you need to configure a virtual 
+To use your IDE for Airflow development and testing, you need to configure a virtual
 environment. Ideally you should set up virtualenv for all Python versions that Airflow
-supports (3.5, 3.6). 
+supports (3.6, 3.7).
 
-Consider using one of the following utilities to create virtual environments and easily 
+Consider using one of the following utilities to create virtual environments and easily
 switch between them with the ``workon`` command:
 
 - `pyenv <https://github.com/pyenv/pyenv>`_
@@ -112,7 +112,7 @@ To create and initialize the local virtualenv:
 
 4. Select the virtualenv you created as the project's default virtualenv in your IDE.
 
-Note that if you have the Breeze development environment installed, the ``breeze`` 
+Note that if you have the Breeze development environment installed, the ``breeze``
 script can automate initializing the created virtualenv (steps 2 and 3).
 Simply enter the Breeze environment by using ``workon`` and, once you are in it, run:
 
@@ -159,7 +159,7 @@ your local sources to the ``/opt/airflow`` location of the sources within the co
 Running Unit Tests via IDE
 --------------------------
 
-Usually you can run most of the unit tests (those that do not have dependencies such as 
+Usually you can run most of the unit tests (those that do not have dependencies such as
 Postgres/MySQL/Hadoop/etc.) directly from the IDE:
 
 .. image:: images/running_unittests.png
@@ -178,9 +178,9 @@ Running Tests via Script
 
 You can also use the ``run-tests`` script that provides a Python
 testing framework with more than 300 tests including integration, unit, and
-system tests. 
+system tests.
 
-The script is in the path in the Breeze environment but you need to prepend 
+The script is in the path in the Breeze environment but you need to prepend
 it with ``./`` when running in the local virtualenv: ``./run-tests``.
 
 This script has several flags that can be useful for your testing.
@@ -224,7 +224,7 @@ or a single test method without colors or debug logs:
 
     ./run-tests tests.core:TestCore.test_check_operators
 
-Note that the first time it runs, the ``./run_tests`` script 
+Note that the first time it runs, the ``./run_tests`` script
 performs a database initialization. If you run further tests without
 leaving the environment, the database will not be initialized. But you
 can always force the database initialization with the ``--with-db-init``
@@ -236,7 +236,7 @@ In general, the ``run-tests`` script can be used to run unit, integration and sy
 Running Unit Tests from the IDE
 -----------------------------------
 
-Once you created the local virtualenv and selected it as the default project's environment, 
+Once you created the local virtualenv and selected it as the default project's environment,
 running unit tests from the IDE is as simple as:
 
 .. figure:: images/run_unittests.png
@@ -252,7 +252,7 @@ system tests. You can technically use local
 virtualenv to run those tests, but it requires to set up a number of
 external components (databases/queues/kubernetes and the like). So, it is
 much easier to use the `Breeze development environment <BREEZE.rst>`_
-for integration and system tests. 
+for integration and system tests.
 
 Note: Soon we will separate the integration and system tests out
 so that you can clearly know which tests are unit tests and can be run in
diff --git a/airflow/contrib/hooks/winrm_hook.py b/airflow/contrib/hooks/winrm_hook.py
index f81756c6fa816..843d080d0e120 100644
--- a/airflow/contrib/hooks/winrm_hook.py
+++ b/airflow/contrib/hooks/winrm_hook.py
@@ -88,7 +88,6 @@ class WinRMHook(BaseHook):
     :param send_cbt: Will send the channel bindings over a HTTPS channel (Default: True)
     :type send_cbt: bool
     """
-
     def __init__(self,
                  ssh_conn_id=None,
                  endpoint=None,
@@ -204,6 +203,7 @@ def get_conn(self):
 
         try:
             if self.password and self.password.strip():
+                # pylint: disable=unexpected-keyword-arg
                 self.winrm_protocol = Protocol(
                     endpoint=self.endpoint,
                     transport=self.transport,
diff --git a/airflow/www/static/docs b/airflow/www/static/docs
deleted file mode 120000
index d5d38018e2486..0000000000000
--- a/airflow/www/static/docs
+++ /dev/null
@@ -1 +0,0 @@
-../../../docs/_build/html/
\ No newline at end of file
diff --git a/breeze b/breeze
index 31174a036a69f..8c1bb5f2a89bb 100755
--- a/breeze
+++ b/breeze
@@ -1,5 +1,4 @@
 #!/usr/bin/env bash
-
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -22,19 +21,6 @@ set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-export AIRFLOW_SOURCES="${MY_DIR}"
-
-# Directory where all CI scripts are located
-export SCRIPTS_CI_DIR="${MY_DIR}/scripts/ci"
-
-BUILD_CACHE_DIR="${MY_DIR}/.build"
-FILES_DIR="${MY_DIR}/files"
-TMP_DIR="${MY_DIR}/tmp"
-
-mkdir -pv "${BUILD_CACHE_DIR}"
-mkdir -pv "${TMP_DIR}"
-mkdir -pv "${FILES_DIR}"
-
 function save_to_file {
     # shellcheck disable=SC2005
     echo "$(eval echo "\$$1")" > "${BUILD_CACHE_DIR}/.$1"
@@ -44,96 +30,99 @@ function read_from_file {
     cat "${BUILD_CACHE_DIR}/.$1" 2>/dev/null || true
 }
 
-export PYTHON_VERSION="${PYTHON_VERSION:=$(read_from_file PYTHON_VERSION)}"
+function initialize_breeze_environment {
+    CMDNAME="$(basename -- "$0")"
+    export CMDNAME
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${SCRIPTS_CI_DIR}/_utils.sh"
+    # Sets width of the screen
+    SEPARATOR_WIDTH="${SEPARATOR_WIDTH:=$(tput cols)}"
+    export SEPARATOR_WIDTH
 
-basic_sanity_checks
+    # Update short and long options in the breeze-complete script
+    # This way autocomplete will work automatically with all options
+    # shellcheck source=breeze-complete
+    . "${MY_DIR}/breeze-complete"
 
-script_start
-
-# Sets width of the screen
-SEPARATOR_WIDTH="$(tput cols)"
+    export AIRFLOW_SOURCES="${MY_DIR}"
 
-# Name of the script
-CMDNAME="$(basename -- "$0")"
+    # Directory where all CI scripts are located
+    export SCRIPTS_CI_DIR="${MY_DIR}/scripts/ci"
 
-# Update short and long options in the breeze-complete script
-# This way autocomplete will work automatically with all options
-# shellcheck source=breeze-complete
-. "${MY_DIR}/breeze-complete"
+    export BUILD_CACHE_DIR="${MY_DIR}/.build"
+    export FILES_DIR="${MY_DIR}/files"
+    export TMP_DIR="${MY_DIR}/tmp"
 
-# Whether to actually run docker compose with the command set given
-ENTER_ENVIRONMENT="true"
+    mkdir -pv "${BUILD_CACHE_DIR}" >/dev/null 2>&1
+    mkdir -pv "${TMP_DIR}" >/dev/null 2>&1
+    mkdir -pv "${FILES_DIR}" >/dev/null 2>&1
 
+    export PYTHON_VERSION="${PYTHON_VERSION:=$(read_from_file PYTHON_VERSION)}"
+}
 
-# Whether to cleanup local image
-CLEANUP_IMAGES="false"
+function setup_default_breeze_variables {
+    # Whether to actually run docker compose with the command set given
+    export COMMAND_TO_RUN="enter_breeze"
 
-# Skips mounting local Airflow sources
-SKIP_MOUNTING_LOCAL_SOURCES="false"
+    # Whether to push images after build
+    export PUSH_IMAGES="false"
 
-# If set, we initialize local virtualenv and install all dependencies
-INITIALIZE_LOCAL_VIRTUALENV=false
+    # Skips mounting local Airflow sources
+    export SKIP_MOUNTING_LOCAL_SOURCES="false"
 
-# If set, we setup autocomplete for breeze
-SETUP_AUTOCOMPLETE=false
+    # Holds chosen command if the -x flag is used.
+    export RUN_COMMAND=""
 
-# Holds chosen command if the -x flag is used.
-RUN_COMMAND=""
+    # Holds the test target if the -t flag is used.
+    export TEST_TARGET=""
 
-# Holds the test target if the -t flag is used.
-TEST_TARGET=""
+    # Holds docker compose command if the -d flag is used.
+    export DOCKER_COMPOSE_COMMAND=""
 
-# Holds docker compose command if the -d flag is used.
-DOCKER_COMPOSE_COMMAND=""
+    # If true, the docker images are rebuilt locally.
+    export DOCKER_BUILD_NEEDED="false"
 
-# If true, the docker images are rebuilt locally.
-export AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="false"
+    # By default we only pull images if we do not have them locally.
+    # This can be overridden by -p flag
+    export FORCE_PULL_IMAGES="false"
 
-# By default we only pull images if we do not have them locally.
-# This can be overridden by -p flag
-export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="false"
+    # Do not start Kubernetes cluster by default
+    export START_KUBERNETES_CLUSTER="false"
 
-# We use docker image caches by default to speed up the builds
-export AIRFLOW_CONTAINER_USE_DOCKER_CACHE=${AIRFLOW_CONTAINER_USE_DOCKER_CACHE:="true"}
+    # Do not recreate Kubernetes cluster by default
+    export RECREATE_KUBERNETES_CLUSTER="false"
 
-# By default we do not push images. This can be overridden by -u flag.
-export AIRFLOW_CONTAINER_PUSH_IMAGES=${AIRFLOW_CONTAINER_PUSH_IMAGES:="false"}
+    # We use docker image caches by default to speed up the builds
+    export USE_DOCKER_CACHE=${USE_DOCKER_CACHE:="true"}
 
-# For local builds we fix file permissions only for setup-related files
-export AIRFLOW_FIX_PERMISSIONS=${AIRFLOW_FIX_PERMISSIONS:="setup"}
+    # Removes kubernetes image if set to "true"
+    export REMOVE_KUBERNETES_IMAGE="false"
 
-# Skip building full CI image locally
-export AIRFLOW_CONTAINER_SKIP_CI_IMAGE="false"
+    # By default we do not push images. This can be overridden by -u flag.
+    export PUSH_IMAGES=${PUSH_IMAGES:="false"}
 
-# Branch name of the base image used (usually master or v1-10-test or v1-10-stable)
-export AIRFLOW_CONTAINER_BRANCH_NAME=${AIRFLOW_CONTAINER_BRANCH_NAME:=${DEFAULT_BRANCH}}
+    # Branch name of the base image used (usually master or v1-10-test or v1-10-stable)
+    export BRANCH_NAME=${BRANCH_NAME:=${DEFAULT_BRANCH}}
 
-# Determine version of the Airflow from version.py
-AIRFLOW_VERSION=$(cat airflow/version.py - << EOF | python
+    # Determine version of the Airflow from version.py
+    AIRFLOW_VERSION=$(cat airflow/version.py - << EOF | python
 print(version.replace("+",""))
 EOF
 )
-export AIRFLOW_VERSION
-
-# Verbosity in running ci scripts
-export VERBOSE="false"
+    export AIRFLOW_VERSION
 
-# Enter environment by default, rather than run tests or bash command or docker compose or static checks
-export RUN_TESTS="false"
-export RUN_DOCKER_COMPOSE="false"
-export RUN_IN_BASH="false"
-export RUN_STATIC_CHECKS="false"
-export RUN_BUILD_DOCS="false"
+    # Verbosity in running ci scripts
+    export VERBOSE="false"
 
-export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD=${AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD:="false"}
+    # Whether to force the build
+    export FORCE_DOCKER_BUILD=${FORCE_DOCKER_BUILD:="false"}
 
-# Files determining whether asciiart/cheatsheet are suppressed
+    # Start airflow-testing image with all the dependencies
+    export DEPS="true"
 
-SUPPRESS_CHEATSHEET_FILE="${MY_DIR}/.suppress_cheatsheet"
-SUPPRESS_ASCIIART_FILE="${MY_DIR}/.suppress_asciiart"
+    # Files determining whether asciiart/cheatsheet are suppressed
+    export SUPPRESS_CHEATSHEET_FILE="${MY_DIR}/.suppress_cheatsheet"
+    export SUPPRESS_ASCIIART_FILE="${MY_DIR}/.suppress_asciiart"
+}
 
 function print_badge {
     if [[ ! -f "${SUPPRESS_ASCIIART_FILE}" ]]; then
@@ -180,83 +169,59 @@ function print_badge {
 &&&&&&&&&&&&&  &&     &&&&@   &&&&&&&&&&&@  &&&&&&&&&&&&  @&&&&&&&&&&&   &&&&&&&&&&&
 
 
-                               Branch name:        ${AIRFLOW_CONTAINER_BRANCH_NAME}
-                               Docker image:       ${AIRFLOW_CONTAINER_DOCKER_IMAGE}
+                               Branch name:        ${BRANCH_NAME}
+                               Docker image:       ${AIRFLOW_CI_IMAGE}
                                Airflow version:    ${AIRFLOW_VERSION}
                                Python version:     ${PYTHON_VERSION}
                                DockerHub user:     ${DOCKERHUB_USER}
                                DockerHub repo:     ${DOCKERHUB_REPO}
                                Backend:            ${BACKEND}
-                               Env:                ${ENV}
 EOF
-        if [[ ${ENV} == "kubernetes" ]]; then
+        if [[ ${START_KUBERNETES_CLUSTER} == "true" ]]; then
             cat <<EOF
 
-                               Kubernetes version: ${KUBERNETES_VERSION}
-                               Kubernetes mode:    ${KUBERNETES_MODE}
-
+                               Starting Kubernetes cluster:
 
+                               Kubernetes mode:    ${KUBERNETES_MODE}
+                               Kubernetes version: ${KUBERNETES_VERSION}
+                               Recreate cluster:   ${RECREATE_KUBERNETES_CLUSTER}
 EOF
         fi
     else
         cat <<EOF
 
-   Branch name:        ${AIRFLOW_CONTAINER_BRANCH_NAME}
-   Docker image:       ${AIRFLOW_CONTAINER_DOCKER_IMAGE}
+   Branch name:        ${BRANCH_NAME}
+   Docker image:       ${AIRFLOW_CI_IMAGE}
    Airflow version:    ${AIRFLOW_VERSION}
    Python version:     ${PYTHON_VERSION}
    DockerHub user:     ${DOCKERHUB_USER}
    DockerHub repo:     ${DOCKERHUB_REPO}
    Backend:            ${BACKEND}
-   Env:                ${ENV}
 EOF
-        if [[ ${ENV} == "kubernetes" ]]; then
+        if [[ ${START_KUBERNETES_CLUSTER} == "true" ]]; then
             cat <<EOF
-   Kubernetes version: ${KUBERNETES_VERSION}
-   Kubernetes mode:    ${KUBERNETES_MODE}
 
+   Starting Kubernetes cluster:
 
+   Kubernetes mode:    ${KUBERNETES_MODE}
+   Kubernetes version: ${KUBERNETES_VERSION}
+   Recreate cluster:   ${RECREATE_KUBERNETES_CLUSTER}
 EOF
         fi
     fi
 }
 
-function prepare_command_file() {
-    local FILE="${1}"
-    local CMD="${2}"
-    local TESTS="${3}"
-    local EXPANSION="${4-@}"
-    cat <<EOF > "${FILE}"
-#!/usr/bin/env bash
-cd "\$(pwd)" || exit
-export DOCKERHUB_USER=${DOCKERHUB_USER}
-export DOCKERHUB_REPO=${DOCKERHUB_REPO}
-export COMPOSE_FILE="${COMPOSE_FILE}"
-export PYTHON_VERSION="${PYTHON_VERSION}"
-export BACKEND="${BACKEND}"
-export ENV="${ENV}"
-export KUBERNETES_VERSION="${KUBERNETES_VERSION}"
-export KUBERNETES_MODE="${KUBERNETES_MODE}"
-export AIRFLOW_VERSION="${AIRFLOW_VERSION}"
-export RUN_TESTS="${TESTS}"
-export WEBSERVER_HOST_PORT="${WEBSERVER_HOST_PORT}"
-export POSTGRES_HOST_PORT="${POSTGRES_HOST_PORT}"
-export MYSQL_HOST_PORT="${MYSQL_HOST_PORT}"
-docker-compose --log-level INFO ${CMD}\$${EXPANSION}"
-EOF
-    chmod u+x "${FILE}"
-}
-
 # Default values
 
-_BREEZE_DEFAULT_ENV="docker"
 _BREEZE_DEFAULT_BACKEND="sqlite"
-_BREEZE_DEFAULT_KUBERNETES_VERSION="v1.13.0"
 _BREEZE_DEFAULT_KUBERNETES_MODE="git_mode"
-
+_BREEZE_DEFAULT_KUBERNETES_VERSION="v1.15.3"
 usage() {
-      echo """
+    WIDTH=$((SEPARATOR_WIDTH>100 ? 100: SEPARATOR_WIDTH))
+    ALLOWED_STATIC_CHECKS=$(echo "${_BREEZE_ALLOWED_STATIC_CHECKS=""}" | tr '\n' ' ' | \
+    fold -w "${WIDTH}" -s)
 
+    echo """
 Usage: ${CMDNAME} [FLAGS] \\
   [-k]|[-S <STATIC_CHECK>]|[-F <STATIC_CHECK>]|[-O]|[-e]|[-a]|[-b]|[-t <TARGET>]|[-x <COMMAND>]|[-d <COMMAND>] \\
   -- <EXTRA_ARGS>
@@ -270,9 +235,9 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
   * Build documentation with -O, --build-docs command
   * Setup local virtualenv with -e, --setup-virtualenv command
   * Setup autocomplete for itself with -a, --setup-autocomplete command
-  * Build docker image with -b, --build-only command
+  * Build CI docker images with -b, --build-only command
   * Run test target specified with -t, --test-target connad
-  * Execute arbitrary command in the test environmenrt with -x, --execute-command command
+  * Execute arbitrary command in the test environment with -x, --execute-command command
   * Execute arbitrary docker-compose command with -d, --docker-compose command
 
 ** Commands
@@ -290,8 +255,10 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
 
 -S, --static-check <STATIC_CHECK>
         Run selected static checks for currently changed files. You should specify static check that
-        you would like to run or 'all' to run all checks. One of
-        [${_BREEZE_ALLOWED_STATIC_CHECKS:=}].
+        you would like to run or 'all' to run all checks. One of:
+
+${ALLOWED_STATIC_CHECKS}
+
         You can pass extra arguments including options to to the pre-commit framework as
         <EXTRA_ARGS> passed after --. For example:
 
@@ -304,8 +271,10 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
 
 -F, --static-check-all-files <STATIC_CHECK>
         Run selected static checks for all applicable files. You should specify static check that
-        you would like to run or 'all' to run all checks. One of
-        [${_BREEZE_ALLOWED_STATIC_CHECKS:=}].
+        you would like to run or 'all' to run all checks. One of:
+
+${ALLOWED_STATIC_CHECKS}
+
         You can pass extra arguments including options to the pre-commit framework as
         <EXTRA_ARGS> passed after --. For example:
 
@@ -327,7 +296,7 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
         shell and when typing breeze command <TAB> will provide autocomplete for parameters and values.
 
 -b, --build-only
-        Only build docker images but do not enter the airflow-testing docker container.
+        Only build CI docker images but do not enter the airflow-testing docker container.
 
 -t, --test-target <TARGET>
         Run the specified unit test target. There might be multiple
@@ -361,22 +330,29 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
         Python version used for the image. This is always major/minor version.
         One of [${_BREEZE_ALLOWED_PYTHON_VERSIONS:=}]. Default is the python3 or python on the path.
 
--E, --env <ENVIRONMENT>
-        Environment to use for tests. It determines which types of tests can be run.
-        One of [${_BREEZE_ALLOWED_ENVS:=}]. Default: ${_BREEZE_DEFAULT_ENV:=}
-
 -B, --backend <BACKEND>
         Backend to use for tests - it determines which database is used.
         One of [${_BREEZE_ALLOWED_BACKENDS:=}]. Default: ${_BREEZE_DEFAULT_BACKEND:=}
 
--K, --kubernetes-version <KUBERNETES_VERSION>
-        Kubernetes version - only used in case of 'kubernetes' environment.
-        One of [${_BREEZE_ALLOWED_KUBERNETES_VERSIONS:=}]. Default: ${_BREEZE_DEFAULT_KUBERNETES_VERSION:=}
+-K, --start-kubernetes-cluster
+        Starts test kubernetes cluster after entering the environment. The cluster is started using
+        Kubernetes Mode selected and Kubernetes version specifed via --kubernetes-mode and
+        --kubernetes-version flags.
+
+-Z, --recreate-kubernetes-cluster
+        Recreates kubernetes cluster if one has already been created. By default if you do not stop
+        environment, the kubernetes cluster created for testing is continuously running and when
+        you start kubernetes testing again it will be reused. You can force deletion and recreation
+        of such cluster with this flag.
 
 -M, --kubernetes-mode <KUBERNETES_MODE>
-        Kubernetes mode - only used in case of 'kubernetes' environment.
+        Kubernetes mode - only used in case --start-kubernetes-cluster flag is specified.
         One of [${_BREEZE_ALLOWED_KUBERNETES_MODES:=}]. Default: ${_BREEZE_DEFAULT_KUBERNETES_MODE:=}
 
+-V, --kubernetes-version <KUBERNETES_VERSION>
+        Kubernetes version - only used in case --start-kubernetes-cluster flag is specified.
+        One of [${_BREEZE_ALLOWED_KUBERNETES_VERSIONS:=}]. Default: ${_BREEZE_DEFAULT_KUBERNETES_VERSION:=}
+
 -s, --skip-mounting-source-volume
         Skips mounting local volume with sources - you get exactly what is in the
         docker image rather than your current local sources of airflow.
@@ -390,6 +366,9 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
 -n, --assume-no
         Assume 'no' answer to all questions.
 
+-q, --assume-quit
+        Assume 'quit' answer to all questions.
+
 -C, --toggle-suppress-cheatsheet
         Toggles on/off cheatsheet displayed before starting bash shell
 
@@ -409,15 +388,15 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
         automatically for the first time or when changes are detected in
         package-related files, but you can force it using this flag.
 
--R, --force-build-images-clean
-        Force build images without cache. This will remove the pulled or build images
-        and start building images from scratch. This might take a long time.
-
 -p, --force-pull-images
         Forces pulling of images from DockerHub before building to populate cache. The
         images are pulled by default only for the first time you run the
         environment, later the locally build images are used as cache.
 
+-L, --use-local-cache
+        Uses local cache to build images. No pulled images will be used, but results of local builds in
+        the Docker cache are used instead.
+
 -u, --push-images
         After building - uploads the images to DockerHub
         It is useful in case you use your own DockerHub user to store images and you want
@@ -430,357 +409,10 @@ The swiss-knife-army tool for Airflow testings. It allows to perform various tes
 """
 }
 
-####################  Parsing options/arguments
-if ! PARAMS=$(getopt \
-    -o "${_BREEZE_GETOPT_SHORT_OPTIONS:=}" \
-    -l "${_BREEZE_GETOPT_LONG_OPTIONS:=}" \
-    --name "$CMDNAME" -- "$@")
-then
-    usage
-    exit 1
-fi
-
-eval set -- "${PARAMS}"
-unset PARAMS
-
-# Parse Flags.
-# Please update short and long options in the breeze-complete script
-# This way autocomplete will work out-of-the-box
-while true
-do
-  case "${1}" in
-    -h|--help)
-      usage;
-      exit 0 ;;
-    -P|--python)
-      export PYTHON_VERSION="${2}";
-      echo
-      echo "Python version: ${PYTHON_VERSION}"
-      echo
-      shift 2 ;;
-    -E|--env)
-      export ENV="${2}";
-      echo
-      echo "Environment: ${ENV}"
-      echo
-      shift 2 ;;
-    -B|--backend)
-      export BACKEND="${2}";
-      echo
-      echo "Backend: ${BACKEND}"
-      echo
-      shift 2 ;;
-    -K|--kubernetes-version)
-      export KUBERNETES_VERSION="${2}";
-      echo
-      echo "Kubernetes version: ${KUBERNETES_VERSION}"
-      echo
-      shift 2 ;;
-    -M|--kubernetes-mode)
-      export KUBERNETES_MODE="${2}";
-      echo
-      echo "Kubernetes mode: ${KUBERNETES_MODE}"
-      echo
-      shift 2 ;;
-    -s|--skip-mounting-local-sources)
-      SKIP_MOUNTING_LOCAL_SOURCES="true"
-      echo "Skip mounting local sources: ${SKIP_MOUNTING_LOCAL_SOURCES}"
-      echo
-      shift ;;
-    -b|--build-only)
-      ENTER_ENVIRONMENT="false"
-      export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-      echo "Only build. Do not enter airflow-testing container"
-      echo
-      shift ;;
-    -v|--verbose)
-      VERBOSE="true"
-      echo "Verbose output"
-      echo
-      shift ;;
-    -y|--assume-yes)
-      export FORCE_ANSWER_TO_QUESTIONS="yes"
-      echo "Assuming 'yes' answer to all questions."
-      echo
-      shift ;;
-    -n|--assume-no)
-      export FORCE_ANSWER_TO_QUESTIONS="no"
-      echo "Assuming 'no' answer to all questions."
-      echo
-      shift ;;
-    -C|--toggle-suppress-cheatsheet)
-      if [[ -f "${SUPPRESS_CHEATSHEET_FILE}" ]]; then
-        rm -f "${SUPPRESS_CHEATSHEET_FILE}"
-      else
-        touch "${SUPPRESS_CHEATSHEET_FILE}"
-      fi
-      echo "Toggle suppress cheatsheet"
-      echo
-      shift ;;
-    -A|--toggle-suppress-asciiart)
-      if [[ -f "${SUPPRESS_ASCIIART_FILE}" ]]; then
-        rm -f "${SUPPRESS_ASCIIART_FILE}"
-      else
-        touch "${SUPPRESS_ASCIIART_FILE}"
-      fi
-      echo "Toggle suppress asciiart"
-      echo
-      shift ;;
-    -r|--force-build-images)
-      echo
-      echo "Force build images"
-      echo
-      export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-      shift ;;
-    -R|--force-build-images-clean)
-      echo
-      echo "Clean build of images without cache"
-      echo
-      export AIRFLOW_CONTAINER_USE_DOCKER_CACHE=false
-      export AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE=false
-      export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-      CLEANUP_IMAGES="true"
-      shift ;;
-    -p|--force-pull-images)
-      echo
-      echo "Force pulling images before build. Uses pulled images as cache."
-      echo
-      export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="true"
-      export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-      shift ;;
-    -u|--push-images)
-      if [[ "${AIRFLOW_FIX_PERMISSIONS}" != "all" ]]; then
-        echo >&2
-        echo >&2 "ERROR: Disable fix permissions when pushing"
-        echo >&2
-        echo >&2 "You cannot push images if you have AIRFLOW_FIX_PERMISSIONS set to other value than 'all'"
-        echo >&2 "Your docker context is most likely wrong in this case"
-        echo >&2 "You need to set AIRFLOW_FIX_PERMISSIONS to false"
-        echo >&2 "And run the build again"
-        echo >&2
-        exit 1
-      fi
-      echo
-      echo "Pushing images to DockerHub"
-      echo
-      export AIRFLOW_CONTAINER_PUSH_IMAGES="true"
-      export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-      shift ;;
-    -c|--cleanup-images)
-      echo
-      echo "Cleanup the images"
-      echo
-      CLEANUP_IMAGES=true
-      shift ;;
-    -D|--dockerhub-user)
-      export DOCKERHUB_USER="${2}"
-      echo
-      echo "Dockerhub user ${DOCKERHUB_USER}"
-      echo
-      shift 2 ;;
-    -H|--dockerhub-repo)
-      export DOCKERHUB_REPO="${2}"
-      echo
-      echo "Dockerhub repo ${DOCKERHUB_REPO}"
-      echo
-      shift 2 ;;
-    -e|--initialize-local-virtualenv)
-      echo
-      echo Initializing local virtualenv
-      echo
-      INITIALIZE_LOCAL_VIRTUALENV="true"
-      SETUP_AUTOCOMPLETE="false"
-      ENTER_ENVIRONMENT=:"false"
-      shift ;;
-    -a|--setup-autocomplete)
-      echo
-      echo Setting up autocomplete
-      echo
-      INITIALIZE_LOCAL_VIRTUALENV="false"
-      SETUP_AUTOCOMPLETE="true"
-      ENTER_ENVIRONMENT=:"false"
-      shift ;;
-    -t|--test-target)
-      export TEST_TARGET="${2}"
-      export RUN_IN_BASH="false"
-      export RUN_TESTS="true"
-      export RUN_DOCKER_COMPOSE="false"
-      export RUN_STATIC_CHECKS="false"
-      export RUN_BUILD_DOCS="false"
-      shift 2 ;;
-    -d|--docker-compose)
-      export DOCKER_COMPOSE_COMMAND="${2}"
-      export RUN_IN_BASH="false"
-      export RUN_TESTS="false"
-      export RUN_DOCKER_COMPOSE="true"
-      export RUN_STATIC_CHECKS="false"
-      export RUN_BUILD_DOCS="false"
-      shift 2 ;;
-    -k|--stop-environment)
-      export DOCKER_COMPOSE_COMMAND="down"
-      export RUN_IN_BASH="false"
-      export RUN_TESTS="false"
-      export RUN_BUILD_DOCS="false"
-      export RUN_DOCKER_COMPOSE="true"
-      export RUN_STATIC_CHECKS="false"
-      shift ;;
-    -x|--execute-command)
-      export RUN_COMMAND="${2}"
-      export RUN_IN_BASH="true"
-      export RUN_TESTS="false"
-      export RUN_BUILD_DOCS="false"
-      export RUN_DOCKER_COMPOSE="false"
-      export RUN_STATIC_CHECKS="false"
-      shift 2 ;;
-    -S|--static-check )
-      export ENTER_ENVIRONMENT="false"
-      export RUN_TESTS="false"
-      export RUN_DOCKER_COMPOSE="false"
-      export RUN_STATIC_CHECKS="true"
-      export RUN_BUILD_DOCS="false"
-      export STATIC_CHECK="${2}"
-      export EXTRA_STATIC_CHECK_OPTIONS=("--show-diff-on-failure")
-      export STATIC_CHECK_ALL_FILES="false"
-      shift 2 ;;
-    -F|--static-check-all-files)
-      export ENTER_ENVIRONMENT="false"
-      export RUN_TESTS="false"
-      export RUN_DOCKER_COMPOSE="false"
-      export RUN_STATIC_CHECKS="true"
-      export RUN_BUILD_DOCS="false"
-      export STATIC_CHECK="${2}"
-      export STATIC_CHECK_ALL_FILES="true"
-      export EXTRA_STATIC_CHECK_OPTIONS=("--all-files" "--show-diff-on-failure")
-      shift 2 ;;
-    -O|--build-docs)
-      export ENTER_ENVIRONMENT="false"
-      export RUN_TESTS="false"
-      export RUN_DOCKER_COMPOSE="false"
-      export RUN_STATIC_CHECKS="false"
-      export RUN_BUILD_DOCS="true"
-      shift 1 ;;
-    --)
-      shift ;
-      break ;;
-    *)
-      usage
-      echo >&2
-      echo >&2 "ERROR: Unknown argument ${1}"
-      echo >&2
-      exit 1
-      ;;
-  esac
-done
-
-echo
-printf '=%.0s' $(seq "${SEPARATOR_WIDTH}")
-echo
-
-CMDNAME="$(basename -- "$0")"
-
-# Cleans up the answer that was given last time, whether to force/
-forget_last_answer
-
-export ENV="${ENV:=$(read_from_file ENV)}"
-export BACKEND="${BACKEND:=$(read_from_file BACKEND)}"
-export KUBERNETES_VERSION="${KUBERNETES_VERSION:=$(read_from_file KUBERNETES_VERSION)}"
-export KUBERNETES_MODE="${KUBERNETES_MODE:=$(read_from_file KUBERNETES_MODE)}"
-
-# Here you read DockerHub user/account that you use
-# You can populate your own images in DockerHub this way and work with the,
-# You can override it with "-d" option and it will be stored in .build directory
-export DOCKERHUB_USER="${DOCKERHUB_USER:=$(read_from_file DOCKERHUB_USER)}"
-export DOCKERHUB_USER="${DOCKERHUB_USER:=${_BREEZE_DEFAULT_DOCKERHUB_USER}}"
-
-# Here you read DockerHub repo that you use
-# You can populate your own images in DockerHub this way and work with them
-# You can override it with "-d" option and it will be stored in .build directory
-export DOCKERHUB_REPO="${DOCKERHUB_REPO:=$(read_from_file DOCKERHUB_REPO)}"
-export DOCKERHUB_REPO="${DOCKERHUB_REPO:=${_BREEZE_DEFAULT_DOCKERHUB_REPO}}"
-
-# Default environment for tests
-export ENV=${ENV:-${_BREEZE_DEFAULT_ENV}}
-
-# Default backend for tests
-export BACKEND=${BACKEND:-${_BREEZE_DEFAULT_BACKEND}}
-
-# Default version of Kubernetes to use
-export KUBERNETES_VERSION=${KUBERNETES_VERSION:=${_BREEZE_DEFAULT_KUBERNETES_VERSION}}
-# Default mode of Kubernetes to use
-export KUBERNETES_MODE=${KUBERNETES_MODE:=${_BREEZE_DEFAULT_KUBERNETES_MODE}}
-
-#################### Check python version ##########################################
-if [[ ${_BREEZE_ALLOWED_PYTHON_VERSIONS:=} != *" ${PYTHON_VERSION} "* ]]; then
-    echo >&2
-    echo >&2 "ERROR:  Allowed Python versions: [${_BREEZE_ALLOWED_PYTHON_VERSIONS}]. Is: '${PYTHON_VERSION}'."
-    echo >&2
-    echo >&2 "Switch to virtualenv with the supported python version or specify python with --python flag."
-    echo >&2
-    exit 1
-fi
-
-#################### Check environments ##########################################
-if [[ ${_BREEZE_ALLOWED_ENVS:=} != *" ${ENV} "* ]]; then
-    echo >&2
-    echo >&2 "ERROR:  Allowed environments are [${_BREEZE_ALLOWED_ENVS}]. Used: '${ENV}'"
-    echo >&2
-    exit 1
-fi
-
-#################### Check backends ##########################################
-if [[ ${_BREEZE_ALLOWED_BACKENDS:=} != *" ${BACKEND} "* ]]; then
-    echo >&2
-    echo >&2 "ERROR:  Allowed backends are [${_BREEZE_ALLOWED_BACKENDS}]. Used: '${BACKEND}'"
-    echo >&2
-    exit 1
-fi
-
-#################### Check environments ##########################################
-if [[ ${_BREEZE_ALLOWED_KUBERNETES_VERSIONS} != *" ${KUBERNETES_VERSION} "* ]]; then
-    echo >&2
-    echo >&2 "ERROR:  Allowed kubernetes versions" \
-         "are [${_BREEZE_ALLOWED_KUBERNETES_VERSIONS}]. Used: '${KUBERNETES_VERSION}'"
-    echo >&2
-    exit 1
-fi
-
-#################### Check environments ##########################################
-if [[ ${_BREEZE_ALLOWED_KUBERNETES_MODES} != *" ${KUBERNETES_MODE} "* ]]; then
-    echo >&2
-    echo >&2 "ERROR:  Allowed kubernetes modes" \
-         "are [${_BREEZE_ALLOWED_KUBERNETES_MODES}]. Used: '${KUBERNETES_MODE}'"
-    echo >&2
-    exit 1
-fi
-
-
-# Those files are mounted into container when run locally
-# .bash_history is preserved and you can modify .bash_aliases and .inputrc
-# according to your liking
-touch "${MY_DIR}/.bash_history"
-touch "${MY_DIR}/.bash_aliases"
-touch "${MY_DIR}/.inputrc"
-
-save_to_file BACKEND
-save_to_file ENV
-save_to_file KUBERNETES_VERSION
-save_to_file KUBERNETES_MODE
-save_to_file PYTHON_VERSION
-save_to_file DOCKERHUB_USER
-save_to_file DOCKERHUB_REPO
-
-#################### Cleanup image if requested ########################################
-if [[ "${CLEANUP_IMAGES}" == "true" ]]; then
-    export AIRFLOW_CONTAINER_CLEANUP_IMAGES=true
-    "${MY_DIR}/scripts/ci/local_ci_cleanup.sh"
-    exit 0
-fi
-
-#################### Initializes local virtualenv ########################################
-if [[ ${INITIALIZE_LOCAL_VIRTUALENV} == "true" ]]; then
+function initialize_virtualenv() {
    # Check if we are in virtualenv
    set +e
-   echo -e "import sys\nif not hasattr(sys,'base_prefix'):\n  sys.exit(1)" | "python${PYTHON_VERSION}"
+   echo -e "import sys\nif not hasattr(sys,'base_prefix'):\n  sys.exit(1)" | python
    RES=$?
    set -e
    if [[ ${RES} != "0" ]]; then
@@ -798,7 +430,7 @@ if [[ ${INITIALIZE_LOCAL_VIRTUALENV} == "true" ]]; then
         echo
         echo "This will wipe out ${AIRFLOW_HOME_DIR} and reset all the databases!"
         echo
-        "${MY_DIR}/confirm" "Proceeding with the initialization"
+        "${MY_DIR}/confirm" "proceed with the initialization"
         echo
         pushd "${MY_DIR}"
         SYSTEM=$(uname -s)
@@ -831,11 +463,9 @@ if [[ ${INITIALIZE_LOCAL_VIRTUALENV} == "true" ]]; then
         airflow db reset -y
         exit 0
    fi
-fi
-
+}
 
-#################### Sets up autocomplete for breeze commands ########################################
-if [[ ${SETUP_AUTOCOMPLETE} == "true" ]]; then
+function setup_autocomplete() {
     echo
     echo "Installing bash/zsh completion for local user"
     echo "Note that completion for zsh is just limited to flags - without their values"
@@ -895,183 +525,613 @@ EOF
     echo "Please re-enter bash or run '. ~/.bash_completion.d/breeze-complete'"
     echo
     exit 0
-fi
+}
 
-MAIN_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose.yml
-BACKEND_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose-${BACKEND}.yml
-LOCAL_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose-local.yml
+function parse_arguments() {
+    if ! PARAMS=$(getopt \
+        -o "${_BREEZE_GETOPT_SHORT_OPTIONS:=}" \
+        -l "${_BREEZE_GETOPT_LONG_OPTIONS:=}" \
+        --name "$CMDNAME" -- "$@")
+    then
+        usage
+        exit 1
+    fi
 
-COMPOSE_FILE=${MAIN_DOCKER_COMPOSE_FILE}:${BACKEND_DOCKER_COMPOSE_FILE}
+    eval set -- "${PARAMS}"
+    unset PARAMS
+
+    # Parse Flags.
+    # Please update short and long options in the breeze-complete script
+    # This way autocomplete will work out-of-the-box
+    while true
+    do
+      case "${1}" in
+        -h|--help)
+          usage;
+          exit 0 ;;
+        -P|--python)
+          export PYTHON_VERSION="${2}";
+          echo "Python version: ${PYTHON_VERSION}"
+          echo
+          shift 2 ;;
+        -B|--backend)
+          export BACKEND="${2}";
+          echo "Backend: ${BACKEND}"
+          echo
+          shift 2 ;;
+        -K|--start-kubernetes-cluster)
+          export START_KUBERNETES_CLUSTER="true";
+          export FORCE_DOCKER_BUILD="true"
+          echo "Starting kubernetes cluster"
+          echo
+          shift ;;
+        -z|--recreate-kubernetes-cluster)
+          export RECREATE_KUBERNETES_CLUSTER="true";
+          echo "Recreate kubernetes cluster"
+          echo
+          shift ;;
+        -M|--kubernetes-mode)
+          export KUBERNETES_MODE="${2}";
+          echo "Kubernetes mode: ${KUBERNETES_MODE}"
+          echo
+          shift 2 ;;
+        -V|--kubernetes-version)
+          export KUBERNETES_VERSION="${2}";
+          echo "Kubernetes version: ${KUBERNETES_VERSION}"
+          echo
+          shift 2 ;;
+        -D|--dockerhub-user)
+          export DOCKERHUB_USER="${2}"
+          echo "Dockerhub user ${DOCKERHUB_USER}"
+          echo
+          shift 2 ;;
+        -H|--dockerhub-repo)
+          export DOCKERHUB_REPO="${2}"
+          echo "Dockerhub repo ${DOCKERHUB_REPO}"
+          echo
+          shift 2 ;;
+        -s|--skip-mounting-local-sources)
+          export SKIP_MOUNTING_LOCAL_SOURCES="true"
+          echo "Skip mounting local sources: ${SKIP_MOUNTING_LOCAL_SOURCES}"
+          echo
+          shift ;;
+        -N|--no-deps)
+          export DEPS="false"
+          echo "Only run airflow-testing image without dependent images"
+          echo
+          shift ;;
+        -b|--build-only)
+          export COMMAND_TO_RUN="build_ci_images_only"
+          export FORCE_DOCKER_BUILD="true"
+          export FORCE_ANSWER_TO_QUESTIONS="yes"
+          echo "Only build CI images without asking. Do not enter airflow-testing container."
+          echo
+          shift ;;
+        -v|--verbose)
+          export VERBOSE="true"
+          echo "Verbose output"
+          echo
+          shift ;;
+        -y|--assume-yes)
+          export FORCE_ANSWER_TO_QUESTIONS="yes"
+          echo "Assuming 'yes' answer to all questions."
+          echo
+          shift ;;
+        -n|--assume-no)
+          export FORCE_ANSWER_TO_QUESTIONS="no"
+          echo "Assuming 'no' answer to all questions."
+          echo
+          shift ;;
+        -q|--assume-quit)
+          export FORCE_ANSWER_TO_QUESTIONS="quit"
+          echo "Assuming 'quit' answer to all questions."
+          echo
+          shift ;;
+        -C|--toggle-suppress-cheatsheet)
+          if [[ -f "${SUPPRESS_CHEATSHEET_FILE}" ]]; then
+            rm -f "${SUPPRESS_CHEATSHEET_FILE}"
+          else
+            touch "${SUPPRESS_CHEATSHEET_FILE}"
+          fi
+          echo "Toggle suppress cheatsheet"
+          echo
+          shift ;;
+        -A|--toggle-suppress-asciiart)
+          if [[ -f "${SUPPRESS_ASCIIART_FILE}" ]]; then
+            rm -f "${SUPPRESS_ASCIIART_FILE}"
+          else
+            touch "${SUPPRESS_ASCIIART_FILE}"
+          fi
+          echo "Toggle suppress asciiart"
+          echo
+          shift ;;
+        -r|--force-build-images)
+          echo "Force build images"
+          echo
+          export FORCE_DOCKER_BUILD="true"
+          shift ;;
+        -L|--use-local-cache)
+          echo "Use local cache to build images"
+          echo
+          export USE_DOCKER_CACHE="false" # TODO Fixme
+          shift ;;
+        -p|--force-pull-images)
+          echo "Force pulling images before build. Uses pulled images as cache."
+          echo
+          export FORCE_PULL_IMAGES="true"
+          export FORCE_DOCKER_BUILD="true"
+          shift ;;
+        -u|--push-images)
+          echo "Pushing images to DockerHub"
+          echo
+          export PUSH_IMAGES="true"
+          export BUILD_IMAGES="true"
+          shift ;;
+        -c|--cleanup-images)
+          echo "Cleanup the images"
+          echo
+          export COMMAND_TO_RUN="cleanup_images"
+          shift ;;
+        -e|--initialize-local-virtualenv)
+          echo "Initializing local virtualenv"
+          echo
+          export COMMAND_TO_RUN="perform_initialize_local_virtualenv"
+          shift ;;
+        -a|--setup-autocomplete)
+          echo "Setting up autocomplete"
+          echo
+          export COMMAND_TO_RUN="perform_setup_autocomplete"
+          shift ;;
+        -t|--test-target)
+          export TEST_TARGET="${2}"
+          if [[ "${TEST_TARGET}" == "." ]]; then
+              export TEST_TARGET=""
+          fi
+          export COMMAND_TO_RUN="run_tests"
+          shift 2 ;;
+        -d|--docker-compose)
+          export DOCKER_COMPOSE_COMMAND="${2}"
+          export COMMAND_TO_RUN="run_docker_compose"
+          shift 2 ;;
+        -k|--stop-environment)
+          export COMMAND_TO_RUN="run_docker_compose"
+          export DOCKER_COMPOSE_COMMAND="down"
+          shift ;;
+        -x|--execute-command)
+          export RUN_COMMAND="${2}"
+          export COMMAND_TO_RUN="run_in_bash"
+          shift 2 ;;
+        -S|--static-check )
+          export COMMAND_TO_RUN="perform_static_checks"
+          export PYTHON_VERSION=${STATIC_CHECK_PYTHON_VERSION}
+          export STATIC_CHECK="${2}"
+          export STATIC_CHECK_ALL_FILES="false"
+          export EXTRA_STATIC_CHECK_OPTIONS=("--show-diff-on-failure")
+          shift 2 ;;
+        -F|--static-check-all-files)
+          export COMMAND_TO_RUN="perform_static_checks"
+          export PYTHON_VERSION=${STATIC_CHECK_PYTHON_VERSION}
+          export STATIC_CHECK="${2}"
+          export STATIC_CHECK_ALL_FILES="true"
+          export EXTRA_STATIC_CHECK_OPTIONS=("--all-files" "--show-diff-on-failure")
+          shift 2 ;;
+        -O|--build-docs)
+          export COMMAND_TO_RUN="build_docs"
+          shift ;;
+        --)
+          shift ;
+          break ;;
+        *)
+          usage
+          echo >&2
+          echo >&2 "ERROR: Unknown argument ${1}"
+          echo >&2
+          exit 1
+          ;;
+      esac
+    done
+    export REMAINING_ARGS=("$@")
+}
 
-if [[ "${SKIP_MOUNTING_LOCAL_SOURCES}" != "true" ]]; then
-    COMPOSE_FILE=${COMPOSE_FILE}:${LOCAL_DOCKER_COMPOSE_FILE}
-fi
+function print_header_line {
+    echo
+    printf '=%.0s' $(seq "${SEPARATOR_WIDTH}")
+    echo
+}
 
-export COMPOSE_FILE
+function print_line {
+    printf '#%.0s' $(seq "${SEPARATOR_WIDTH}")
+}
+
+function read_saved_environment_variables {
+    # Here you read DockerHub user/account that you use
+    # You can populate your own images in DockerHub this way and work with the,
+    # You can override it with "-d" option and it will be stored in .build directory
+    export DOCKERHUB_USER="${DOCKERHUB_USER:=$(read_from_file DOCKERHUB_USER)}"
+    export DOCKERHUB_USER="${DOCKERHUB_USER:=${_BREEZE_DEFAULT_DOCKERHUB_USER}}"
+
+    # Here you read DockerHub repo that you use
+    # You can populate your own images in DockerHub this way and work with them
+    # You can override it with "-d" option and it will be stored in .build directory
+    export DOCKERHUB_REPO="${DOCKERHUB_REPO:=$(read_from_file DOCKERHUB_REPO)}"
+    export DOCKERHUB_REPO="${DOCKERHUB_REPO:=${_BREEZE_DEFAULT_DOCKERHUB_REPO}}"
+
+    # Default backend for tests
+    export BACKEND="${BACKEND:=$(read_from_file BACKEND)}"
+    export BACKEND=${BACKEND:-${_BREEZE_DEFAULT_BACKEND}}
+
+    # Default mode of Kubernetes to use
+    export KUBERNETES_MODE="${KUBERNETES_MODE:=$(read_from_file KUBERNETES_MODE)}"
+    export KUBERNETES_MODE=${KUBERNETES_MODE:=${_BREEZE_DEFAULT_KUBERNETES_MODE}}
+
+    # Default Kubernetes version to use
+    export KUBERNETES_VERSION="${KUBERNETES_VERSION:=$(read_from_file KUBERNETES_VERSION)}"
+    export KUBERNETES_VERSION=${KUBERNETES_VERSION:=${_BREEZE_DEFAULT_KUBERNETES_VERSION}}
+}
+
+function validate_parameters {
+    #################### Check python version ##########################################
+    if [[ ${_BREEZE_ALLOWED_PYTHON_VERSIONS:=} != *" ${PYTHON_VERSION} "* ]]; then
+        echo >&2
+        echo >&2 "ERROR:  Allowed Python versions: [${_BREEZE_ALLOWED_PYTHON_VERSIONS}]. Is: '${PYTHON_VERSION}'."
+        echo >&2
+        echo >&2 "Switch to virtualenv with the supported python version or specify python with --python flag."
+        echo >&2
+        rm -f "${BUILD_CACHE_DIR}/.PYTHON_VERSION"exit 1
+    fi
+
+    #################### Check backends ##########################################
+    if [[ ${_BREEZE_ALLOWED_BACKENDS:=} != *" ${BACKEND} "* ]]; then
+        echo >&2
+        echo >&2 "ERROR:  Allowed backends are [${_BREEZE_ALLOWED_BACKENDS}]. Used: '${BACKEND}'"
+        echo >&2
+        rm -f "${BUILD_CACHE_DIR}/.BACKEND"exit 1
+    fi
+
+    #################### Check kubernetes mode ##########################################
+    if [[ ${_BREEZE_ALLOWED_KUBERNETES_MODES} != *" ${KUBERNETES_MODE} "* ]]; then
+        echo >&2
+        echo >&2 "ERROR:  Allowed kubernetes modes" \
+             "are [${_BREEZE_ALLOWED_KUBERNETES_MODES}]. Used: '${KUBERNETES_MODE}'"
+        echo >&2
+        rm -f "${BUILD_CACHE_DIR}/.KUBERNETES_MODE"exit 1
+    fi
 
-CI_ENTRYPOINT_FILE="/opt/airflow/scripts/ci/in_container/entrypoint_ci.sh"
+    #################### Check kubernetes version ##########################################
+    if [[ ${_BREEZE_ALLOWED_KUBERNETES_VERSIONS} != *" ${KUBERNETES_VERSION} "* ]]; then
+        echo >&2
+        echo >&2 "ERROR:  Allowed kubernetes versions" \
+             "are [${_BREEZE_ALLOWED_KUBERNETES_VERSIONS}]. Used: '${KUBERNETES_VERSION}'"
+        echo >&2
+        rm -f "${BUILD_CACHE_DIR}/.KUBERNETES_VERSION"exit 1
+    fi
+
+}
+
+function save_breeze_environment_variables {
+    save_to_file BACKEND
+    save_to_file KUBERNETES_MODE
+    save_to_file KUBERNETES_VERSION
+    save_to_file PYTHON_VERSION
+    save_to_file DOCKERHUB_USER
+    save_to_file DOCKERHUB_REPO
+}
+
+function touch_local_files {
+    # Those files are mounted into container when run locally
+    # .bash_history is preserved and you can modify .bash_aliases and .inputrc
+    # according to your liking
+    if [[ -d "${MY_DIR}/.bash_history" ]]; then
+        rm -rf "${MY_DIR}/.bash_history"
+    fi
+    touch "${MY_DIR}/.bash_history"
+    touch "${MY_DIR}/.bash_aliases"
+    touch "${MY_DIR}/.inputrc"
+    mkdir -pv "${MY_DIR}/.kube"
+}
+
+function prepare_command_file() {
+    local FILE="${1}"
+    local CMD="${2}"
+    local TESTS="${3}"
+    local EXPANSION="${4-@}"
+    cat <<EOF > "${FILE}"
+#!/usr/bin/env bash
+cd "\$(pwd)" || exit
+export DOCKERHUB_USER=${DOCKERHUB_USER}
+export DOCKERHUB_REPO=${DOCKERHUB_REPO}
+export COMPOSE_FILE="${COMPOSE_FILE}"
+export PYTHON_VERSION="${PYTHON_VERSION}"
+export BACKEND="${BACKEND}"
+export START_KUBERNETES_CLUSTER="${START_KUBERNETES_CLUSTER}"
+export KUBERNETES_MODE="${KUBERNETES_MODE}"
+export KUBERNETES_VERSION="${KUBERNETES_VERSION}"
+export AIRFLOW_VERSION="${AIRFLOW_VERSION}"
+export RUN_TESTS="${TESTS}"
+export WEBSERVER_HOST_PORT="${WEBSERVER_HOST_PORT}"
+export POSTGRES_HOST_PORT="${POSTGRES_HOST_PORT}"
+export MYSQL_HOST_PORT="${MYSQL_HOST_PORT}"
+export AIRFLOW_CI_IMAGE="${AIRFLOW_CI_IMAGE}"
+export AIRFLOW_CI_KUBERNETES_IMAGE="${AIRFLOW_CI_KUBERNETES_IMAGE}"
+docker-compose --log-level INFO ${CMD}\$${EXPANSION}"
+EOF
+    chmod u+x "${FILE}"
+}
 
-DC_RUN_COMMAND="run --service-ports --rm airflow-testing \"${CI_ENTRYPOINT_FILE} "
-DC_RUN_COMMAND_ONLY_AT="run --no-deps --service-ports --rm airflow-testing \"${CI_ENTRYPOINT_FILE} "
+function prepare_command_files {
+    MAIN_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose.yml
+    BACKEND_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose-${BACKEND}.yml
+    LOCAL_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose-local.yml
+    KUBERNETES_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose-kubernetes.yml
 
-LAST_DC_RUN_FILE="cmd_run"
-LAST_DC_RUN_ONLY_AT_FILE="cmd_only_at_run"
-LAST_DC_TEST_FILE="test_run"
-LAST_DC_FILE="dc"
+    COMPOSE_FILE=${MAIN_DOCKER_COMPOSE_FILE}:${BACKEND_DOCKER_COMPOSE_FILE}
 
-# Prepare script for "run command"
-prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}" "${DC_RUN_COMMAND}" "false" '*'
+    if [[ "${SKIP_MOUNTING_LOCAL_SOURCES}" != "true" ]]; then
+        COMPOSE_FILE=${COMPOSE_FILE}:${LOCAL_DOCKER_COMPOSE_FILE}
+    fi
 
-# Prepare script for "run command"
-prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_RUN_ONLY_AT_FILE}" "${DC_RUN_COMMAND_ONLY_AT}" "false" '*'
+    if [[ ${START_KUBERNETES_CLUSTER} == "true" ]]; then
+        COMPOSE_FILE=${COMPOSE_FILE}:${KUBERNETES_DOCKER_COMPOSE_FILE}
+    fi
 
-# Prepare script for "run test"
-prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE}" "${DC_RUN_COMMAND}" "true" '*'
+    export COMPOSE_FILE
 
-# Prepare script for "run docker compose command"
-prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_FILE}" '"' "false"
+    CI_ENTRYPOINT_FILE="/opt/airflow/scripts/ci/in_container/entrypoint_ci.sh"
 
-rebuild_ci_image_if_needed
+    DC_RUN_COMMAND="run --service-ports --rm airflow-testing \"${CI_ENTRYPOINT_FILE} "
+    DC_RUN_COMMAND_NO_DEPS="run --no-deps --service-ports --rm airflow-testing \"${CI_ENTRYPOINT_FILE} "
 
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
-${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
+    export LAST_DC_RUN_FILE="cmd_run"
+    export LAST_DC_RUN_NO_DEPS_FILE="cmd_only_no_deps"
+    export LAST_DC_TEST_FILE="test_run"
+    export LAST_DC_FILE="docker_compose"
 
-printf '=%.0s' $(seq "${SEPARATOR_WIDTH}")
+    # Prepare script for "run command"
+    prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}" "${DC_RUN_COMMAND}" "false" '*'
 
-if [[ "${TEST_TARGET}" == "." ]]; then
-    TEST_TARGET=""
-fi
+    # Prepare script for "run command without dependencies"
+    prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_RUN_NO_DEPS_FILE}" "${DC_RUN_COMMAND_NO_DEPS}" "false" '*'
 
-print_badge
+    # Prepare script for "run test"
+    prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE}" "${DC_RUN_COMMAND}" "true" '*'
 
-function print_line {
-    printf '#%.0s' $(seq "${SEPARATOR_WIDTH}")
+    # Prepare script for "run docker compose command"
+    prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_FILE}" '"' "false"
 }
 
-if [[ ! -f ${SUPPRESS_CHEATSHEET_FILE} ]]; then
-    echo
-    echo
-    print_line
-    echo
-    echo "                                  Airflow Breeze CHEATSHEET"
-    echo
-    print_line
-    echo
-    echo
-    print_line
-    echo
-    echo " Quick scripts:"
-    echo "    * Enter the full environment        : ${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}"
-    echo "    * Run command in full environment   : ${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE} "\
-                                                     "[command with args] [bash options]"
-    echo "    * Run command airflow-testing only  : ${BUILD_CACHE_DIR}/${LAST_DC_RUN_ONLY_AT_FILE} "\
-                                                     "[command with args] [bash options]"
-    echo "    * Run tests in the full environment : ${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE} "\
-                                                     "[test target] [nosetest options]"
-    echo "    * Run Docker compose command        : ${BUILD_CACHE_DIR}/${LAST_DC_FILE} "\
-                                                     "[docker compose command] [docker-compose options]"
-    echo
+function print_cheatsheet {
+    if [[ ! -f ${SUPPRESS_CHEATSHEET_FILE} ]]; then
+        echo
+        echo
+        print_line
+        echo
+        echo "                                  Airflow Breeze CHEATSHEET"
+        echo
+        print_line
+        echo
+        echo
+        print_line
+        echo
+        echo " Quick scripts:"
+        echo "    * Enter the full environment            : ${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}"
+        echo "    * Run command in full environment       : ${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE} "\
+                                                              "[command with args] [bash options]"
+        echo "    * Run command without dependent images  : ${BUILD_CACHE_DIR}/${LAST_DC_RUN_NO_DEPS_FILE} "\
+                                                              "[command with args] [bash options]"
+        echo "    * Run tests in the full environment     : ${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE} "\
+                                                              "[test target] [nosetest options]"
+        echo "    * Run Docker compose command            : ${BUILD_CACHE_DIR}/${LAST_DC_FILE} "\
+                                                              "[docker-compose cmd] [docker-compose options]"
+        echo
 
-    set +e
-    if ! command -v breeze; then
+        set +e
+        if ! command -v breeze; then
+            print_line
+            echo
+            echo " Adding breeze to your path:"
+            echo "    When you exit the environment, you can add sources of airflow to the path - you can"
+            echo "    run breeze or the scripts above from any directory by calling 'breeze' commands directly"
+            echo
+            echo "     export PATH=\${PATH}:\"${MY_DIR}\""
+            echo
+        fi
+        set -e
         print_line
+
+        echo
+        echo " Port forwarding:"
         echo
-        echo " Adding breeze to your path:"
-        echo "    When you exit the environment, you can add sources of airflow to the path - you can"
-        echo "    run breeze or the scripts above from any directory by calling 'breeze' commands directly"
+        echo "   Ports are forwarded to the running docker containers for webserver and database"
+        echo "     * ${WEBSERVER_HOST_PORT} -> forwarded to airflow webserver -> airflow-testing:8080"
+        echo "     * ${POSTGRES_HOST_PORT} -> forwarded to postgres database -> postgres:5432"
+        echo "     * ${MYSQL_HOST_PORT} -> forwarded to mysql database  -> mysql:3306"
         echo
-        echo "     export PATH=\${PATH}:\"${MY_DIR}\""
+        echo "   Here are links to those services that you can use on host:"
+        echo "     * Webserver: http://127.0.0.1:28080"
+        echo "     * Postgres:  jdbc:postgresql://127.0.0.1:25433/airflow?user=postgres&password=airflow"
+        echo "     * Mysql:     jdbc:mysql://localhost:23306/airflow?user=root"
+        echo
+    else
         echo
     fi
-    set -e
-    print_line
+}
 
+function print_setup_instructions {
+    # shellcheck disable=SC2034  # Unused variables left for comp_breeze usage
+    if ! typeset -f "_comp_breeze" > /dev/null; then
+        print_line
+        echo
+        echo "  You can setup autocomplete by running '$0 --setup-autocomplete'"
+        echo
+        echo
+    fi
+    print_line
     echo
-    echo " Port forwarding:"
+    echo "  You can toggle ascii/cheatsheet by adding this flag:"
+    echo "      * --toggle-suppress-cheatsheet"
+    echo "      * --toggle-suppress-asciiart"
     echo
-    echo "   Ports are forwarded to the running docker containers for webserver and database"
-    echo "     * ${WEBSERVER_HOST_PORT} -> forwarded to airflow webserver -> airflow-testing:8080"
-    echo "     * ${POSTGRES_HOST_PORT} -> forwarded to postgres database -> postgres:5432"
-    echo "     * ${MYSQL_HOST_PORT} -> forwarded to mysql database  -> mysql:3306"
+    print_line
     echo
-    echo "   Here are links to those services that you can use on host:"
-    echo "     * Webserver: http://127.0.0.1:28080"
-    echo "     * Postgres:  jdbc:postgresql://127.0.0.1:25433/airflow?user=postgres&password=airflow"
-    echo "     * Mysql:     jdbc:mysql://localhost:23306/airflow?user=root"
     echo
-else
     echo
-fi
-
-# shellcheck disable=SC2034  # Unused variables left for comp_breeze usage
-if ! typeset -f "_comp_breeze" > /dev/null; then
-    print_line
     echo
-    echo "  You can setup autocomplete by running '$0 --setup-autocomplete'"
+}
+
+function make_sure_precommit_is_installed {
     echo
+    echo "Making sure pre-commit is installed"
     echo
-fi
-print_line
-echo
-echo "  You can toggle ascii/cheatsheet by adding this flag:"
-echo "      * --toggle-suppress-cheatsheet"
-echo "      * --toggle-suppress-asciiart"
-echo
-print_line
-echo
-echo
-echo
-echo
-
-if [[ ${ENTER_ENVIRONMENT} == "true" ]]; then
-    if [[ ${RUN_TESTS} == "true" ]]; then
-        "${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE}" "\"${TEST_TARGET}\"" "$@"
-    elif [[ ${RUN_DOCKER_COMPOSE} == "true" ]]; then
-        "${BUILD_CACHE_DIR}/${LAST_DC_FILE}" "${DOCKER_COMPOSE_COMMAND}" "$@"
-    elif [[ ${RUN_IN_BASH} == "true" ]]; then
-        "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}" "${RUN_COMMAND}" "$@"
+    if command -v pip3 >/dev/null; then
+      PIP_BIN=pip3
+    elif command -v pip >/dev/null; then
+      PIP_BIN=pip
     else
-        "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}"
+      echo >&2
+      echo >&2 "ERROR: You need to have pip or pip3 in your PATH"
+      echo >&2
+      S
+      exit 1
     fi
-else
-    if [[ ${RUN_STATIC_CHECKS} == "true" ]]; then
+    "${PIP_BIN}" install --user --upgrade pre-commit >/dev/null 2>&1
+    # Add ~/.local/bin to the path in case pip is run outside of virtualenv
+    export PATH="${PATH}":~/.local/bin
+}
+
+
+function run_static_checks {
+    if [[ ${STATIC_CHECK} == "all" ]]; then
         echo
-        echo "Making sure pre-commit is installed"
+        echo "Running: pre-commit run" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
         echo
-        if command -v pip3 >/dev/null; then
-          PIP_BIN=pip3
-        elif command -v pip >/dev/null; then
-          PIP_BIN=pip
-        else
+
+    elif [[ ${STATIC_CHECK} == "all-but-pylint" ]]; then
+        echo
+        echo "Setting SKIP=pylint. Running: pre-commit run" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
+        echo
+        echo
+        SKIP=pylint pre-commit run "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
+    else
+        echo
+        echo "Running: pre-commit run" "${STATIC_CHECK}" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
+        echo
+        pre-commit run "${STATIC_CHECK}" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
+    fi
+}
+
+function run_build_command {
+    case "${COMMAND_TO_RUN}" in
+        enter_breeze|build_docs|run_tests|run_docker_compose|run_in_bash)
+            rebuild_ci_image_if_needed
+            ;;
+        perform_static_checks|build_ci_images_only)
+            rebuild_ci_image_if_needed
+            ;;
+        cleanup_images)
+            remove_all_images
+            ;;
+        perform_initialize_local_virtualenv|perform_setup_autocomplete)
+            # skip building images
+            ;;
+        *)
           echo >&2
-          echo >&2 "ERROR: You need to have pip or pip3 in your PATH"
+          echo >&2 "ERROR: Unknown command to run ${COMMAND_TO_RUN}"
           echo >&2
-          S
-          exit 1
-        fi
-        "${PIP_BIN}" install --upgrade pre-commit >/dev/null 2>&1
-        # Add ~/.local/bin to the path in case pip is run outside of virtualenv
-        export PATH="${PATH}":~/.local/bin
-        if [[ ${STATIC_CHECK} == "all" ]]; then
-            echo
-            echo "Running: pre-commit run" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
-            echo
-            pre-commit run "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
-        elif [[ ${STATIC_CHECK} == "all-but-pylint" ]]; then
-            echo
-            echo "Setting SKIP=pylint. Running: pre-commit run" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
-            echo
-            echo
-            SKIP=pylint pre-commit run "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
-        else
-            echo
-            echo "Running: pre-commit run" "${STATIC_CHECK}" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
-            echo
-            pre-commit run "${STATIC_CHECK}" "${EXTRA_STATIC_CHECK_OPTIONS[@]}" "$@"
-        fi
-    elif [[  ${RUN_BUILD_DOCS} == "true" ]]; then
-        run_docs
-    fi
-fi
+          ;;
+    esac
+}
+
+function run_breeze_command {
+    set -u
+    case "${COMMAND_TO_RUN}" in
+        enter_breeze)
+            if [[ ${DEPS} == "true" ]]; then
+                 "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}"
+            else
+                 "${BUILD_CACHE_DIR}/${LAST_DC_RUN_NO_DEPS_FILE}"
+            fi
+            ;;
+        run_tests)
+            "${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE}" "\"${TEST_TARGET}\"" "$@"
+            ;;
+        run_docker_compose)
+            "${BUILD_CACHE_DIR}/${LAST_DC_FILE}" "${DOCKER_COMPOSE_COMMAND}" "$@"
+            ;;
+        run_in_bash)
+            if [[ ${DEPS} == "true" ]]; then
+                 "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}" "${RUN_COMMAND}" "$@"
+            else
+                 "${BUILD_CACHE_DIR}/${LAST_DC_RUN_NO_DEPS_FILE}" "${RUN_COMMAND}" "$@"
+            fi
+            ;;
+        perform_static_checks)
+            make_sure_precommit_is_installed
+            run_static_checks "$@"
+            ;;
+        build_ci_images_only)
+            if [[ ${PUSH_IMAGES} == "true" ]]; then
+                push_ci_image
+            fi
+            ;;
+        cleanup_images)
+            ;;
+        perform_initialize_local_virtualenv)
+            initialize_virtualenv
+            ;;
+        perform_setup_autocomplete)
+            setup_autocomplete
+            ;;
+        build_docs)
+            run_docs
+            ;;
+        *)
+          echo >&2
+          echo >&2 "ERROR: Unknown command to run ${COMMAND_TO_RUN}"
+          echo >&2
+          ;;
+    esac
+}
+
+initialize_breeze_environment
+
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/scripts/ci/utils/_include_all.sh"
+
+setup_default_breeze_variables
+
+parse_arguments "$@"
+
+read_saved_environment_variables
+
+script_start
+
+initialize_environment
+
+prepare_build
+
+set_image_variables
+
+prepare_run
+
+prepare_command_files
+
+print_header_line
+
+validate_parameters
+
+save_breeze_environment_variables
+
+touch_local_files
+
+run_build_command
+
+print_header_line
+
+print_badge
+
+print_setup_instructions
+
+set +u  # Account for an empty array
+run_breeze_command "${REMAINING_ARGS[@]}"
 
 script_end
diff --git a/breeze-complete b/breeze-complete
index 49f7babbe329f..cab3d500ca48f 100644
--- a/breeze-complete
+++ b/breeze-complete
@@ -18,30 +18,62 @@
 # under the License.
 
 _BREEZE_ALLOWED_PYTHON_VERSIONS=" 3.6 3.7 "
-_BREEZE_ALLOWED_ENVS=" docker kubernetes "
 _BREEZE_ALLOWED_BACKENDS=" sqlite mysql postgres "
-_BREEZE_ALLOWED_KUBERNETES_VERSIONS=" v1.13.0 "
 _BREEZE_ALLOWED_KUBERNETES_MODES=" persistent_mode git_mode "
-_BREEZE_ALLOWED_STATIC_CHECKS=" all all-but-pylint check-hooks-apply check-merge-conflict check-executables-have-shebangs check-xml detect-private-key doctoc end-of-file-fixer flake8 forbid-tabs insert-license check-apache-license lint-dockerfile mixed-line-ending mypy pylint shellcheck"
+_BREEZE_ALLOWED_KUBERNETES_VERSIONS=" v1.15.3 v1.16.2 "
+_BREEZE_ALLOWED_STATIC_CHECKS=$(cat <<-EOF
+all
+all-but-pylint
+build
+check-apache-license
+check-executables-have-shebangs
+check-hooks-apply
+check-merge-conflict
+check-xml
+detect-private-key
+doctoc
+end-of-file-fixer
+flake8
+forbid-tabs
+insert-license
+isort
+lint-dockerfile
+mixed-line-ending
+mypy
+pylint
+python-no-log-warn
+rst-backticks
+shellcheck
+yamllint
+EOF
+)
 _BREEZE_DEFAULT_DOCKERHUB_USER="apache"
 _BREEZE_DEFAULT_DOCKERHUB_REPO="airflow"
 
 _BREEZE_SHORT_OPTIONS="
-h P: E: B: K: M:
-s b O
-v y n C A
-r R p u
-c D: H: e a
-t: d: k x: S: F:
+h
+P: B:
+K Z M: V:
+D: H:
+s N v y n q
+C A
+r p L u
+b O c k
+e a
+t: S: F: d: x:
 "
 
 _BREEZE_LONG_OPTIONS="
-help python: env: backend: kubernetes-version: kubernetes-mode:
-skip-mounting-local-sources build-only build-docs
-verbose assume-yes assume-no toggle-suppress-cheatsheet toggle-suppress-asciiart
-force-build-images force-build-images-clean force-pull-images push-images
-cleanup-images dockerhub-user: dockerhub-repo: initialize-local-virtualenv setup-autocomplete
-test-target: docker-compose: stop-environment execute-command: static-check: static-check-all-files:
+help
+python: backend:
+start-kubernetes-cluster recreate-kubernetes-cluster kubernetes-mode: kubernetes-version:
+dockerhub-user: dockerhub-repo:
+skip-mounting-local-sources no-deps verbose assume-yes assume-no assume-quit
+toggle-suppress-cheatsheet toggle-suppress-asciiart
+force-build-images force-pull-images use-local-cache push-images
+build-only build-docs cleanup-images stop-environment
+initialize-local-virtualenv setup-autocomplete
+test-target: static-check: static-check-all-files: docker-compose: execute-command:
 "
 
 # Note on OSX bash has no associative arrays (Bash 3.2) so we have to fake it
@@ -52,14 +84,16 @@ function _get_known_values_breeze {
     case "$1" in
         -P | --python )
             _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_PYTHON_VERSIONS} ;;
-        -E | --env )
-            _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_ENVS} ;;
         -B | --backend )
             _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_BACKENDS} ;;
-        -K | --kubernetes-version )
-            _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_KUBERNETES_VERSIONS} ;;
         -M | --kubernetes-mode )
             _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_KUBERNETES_MODES} ;;
+        -V | --kubernetes-version )
+            _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_KUBERNETES_VERSIONS} ;;
+        -D | --dockerhub-user )
+            _BREEZE_KNOWN_VALUES="${_BREEZE_DEFAULT_DOCKERHUB_USER}" ;;
+        -H | --dockerhub-repo )
+            _BREEZE_KNOWN_VALUES="${_BREEZE_DEFAULT_DOCKERHUB_REPO}" ;;
         -S | --static-check )
             _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_STATIC_CHECKS} ;;
         -F | --static-check-all-files )
@@ -70,10 +104,6 @@ function _get_known_values_breeze {
               _docker_compose
             fi
             _BREEZE_KNOWN_VALUES="" ;;
-        -D | --dockerhub-user )
-            _BREEZE_KNOWN_VALUES="${_BREEZE_DEFAULT_DOCKERHUB_USER}" ;;
-        -H | --dockerhub-repo )
-            _BREEZE_KNOWN_VALUES="${_BREEZE_DEFAULT_DOCKERHUB_REPO}" ;;
         *)
             _BREEZE_KNOWN_VALUES=""
     esac
diff --git a/common/_autodetect_variables.sh b/common/_autodetect_variables.sh
deleted file mode 100644
index 9e5b1da65857a..0000000000000
--- a/common/_autodetect_variables.sh
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# This autodetection of PYTHON_VERSION works in this sequence:
-#
-#    1) When running builds on DockerHub we cannot pass different environment
-#       variables there, so we detect python version based on the image
-#       name being build (airflow:master-python3.7 -> PYTHON_VERSION=3.7).
-#       DockerHub adds index.docker.io in front of the image name.
-#
-#    2) When running builds on Travis CI. We use python version determined
-#       from default python3 version available on the path. This way we
-#       do not have to specify PYTHON_VERSION separately in each job.
-#       We just specify which host python version is used for that job.
-#       This makes a nice UI experience where you see python version in
-#       Travis UI.
-#
-#    3) Running builds locally via scripts where we can pass PYTHON_VERSION
-#       as environment variable.
-#
-#    4) Running builds locally for the first time with Breeze. By default
-#       we determine the version based on default python3 version we have
-#       in the host system (3.6 or 3.7) and we use this one.
-#
-#    5) Selecting python version with Breeze's --python switch. This will
-#       override python version but it will also store the last used version
-#       of python in .build directory so that it is automatically used next
-#       time.
-
-# shellcheck source=common/_default_branch.sh
-. "${AIRFLOW_SOURCES}/common/_default_branch.sh"
-
-# You can override DOCKERHUB_USER to use your own DockerHub account and play with your
-# own docker images. In this case you can build images locally and push them
-export DOCKERHUB_USER=${DOCKERHUB_USER:="apache"}
-
-# You can override DOCKERHUB_REPO to use your own DockerHub repository and play with your
-# own docker images. In this case you can build images locally and push them
-export DOCKERHUB_REPO=${DOCKERHUB_REPO:="airflow"}
-
-# if AIRFLOW_CONTAINER_BRANCH_NAME is not set it will be set to either SOURCE_BRANCH
-# (if overridden by configuration) or default branch configured in /common/_default_branch.sh
-export SOURCE_BRANCH=${SOURCE_BRANCH:=${DEFAULT_BRANCH}}
-
-export AIRFLOW_CONTAINER_BRANCH_NAME=${AIRFLOW_CONTAINER_BRANCH_NAME:=${SOURCE_BRANCH}}
-
-# IMAGE_NAME might come from DockerHub and we decide which PYTHON_VERSION to use based on it (see below)
-# In case IMAGE_NAME is not set we determine PYTHON_VERSION based on the default python on the path
-# So in virtualenvs it will use the virtualenv's PYTHON_VERSION and in DockerHub it will determine
-# PYTHON_VERSION from the IMAGE_NAME set in the DockerHub build.
-# See comment above in PYTHON_VERSION - we will be able to get rid of this cumbersomness when we switch
-# to a different CI system and start pushing images to DockerHub rather than build it there.
-export BASE_IMAGE_NAME=${IMAGE_NAME:=${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION:-3.6}}
-
-# Remove index.docker.io/ prefix as it is added by default by DockerHub
-export LOCAL_BASE_IMAGE_NAME=${BASE_IMAGE_NAME#index.docker.io/}
-
-if [[ ! ${LOCAL_BASE_IMAGE_NAME} == ${DOCKERHUB_USER}/${DOCKERHUB_REPO}* ]]; then
-    echo >&2
-    echo >&2 "ERROR:  The ${LOCAL_BASE_IMAGE_NAME} does not start with ${DOCKERHUB_USER}/${DOCKERHUB_REPO}"
-    echo >&2
-    exit 1
-fi
-
-# Extract IMAGE_TAG from LOCAL_BASE_IMAGE_NAME (apache/airflow:master-python3.6 -> master-python3.6)
-export IMAGE_TAG=${LOCAL_BASE_IMAGE_NAME##${DOCKERHUB_USER}/${DOCKERHUB_REPO}:}
-
-# Extract TAG_PREFIX from IMAGE_TAG (master-python3.6 -> master)
-export TAG_PREFIX=${IMAGE_TAG%-*}
-
-# By that time no matter if we got the version from the IMAGE_NAME or we built
-# IMAGE_NAME with PYTHON_VERSION - the LOCAL_BASE_IMAGE_NAME should end with the correct pythonX.Y
-# So we Extract python version from image name. The bash construct below extracts last
-# field after '-' delimiter, so for example 'airflow:master-python3.6'
-# will produce LONG_PYTHON_VERSION_FROM_IMAGE=python3.6
-export LONG_PYTHON_VERSION_FROM_IMAGE="${LOCAL_BASE_IMAGE_NAME##*-}"
-
-# Verify that python version retrieved from image follows the expected pattern of python3.Y
-if [[ ! ${LONG_PYTHON_VERSION_FROM_IMAGE} =~ python[23]\.[0-9]+ ]]; then
-    echo >&2
-    echo >&2 "ERROR:  Python version extracted from IMAGE_NAME does not match the python3.Y format"
-    echo >&2
-    echo >&2 "The IMAGE_NAME format should be '<BRANCH>-python[23].Y'"
-    echo >&2
-    exit 1
-fi
-
-# Unless python_version is forced, read actual python version from IMAGE NAME
-export PYTHON_VERSION=${LONG_PYTHON_VERSION_FROM_IMAGE#python}
diff --git a/confirm b/confirm
index d758f38368626..3b65145aaf6cd 100755
--- a/confirm
+++ b/confirm
@@ -17,7 +17,7 @@
 # under the License.
 set -euo pipefail
 
-if [[ "${FORCE_ANSWER_TO_QUESTIONS:=""}" != "" ]]; then
+if [[ -n "${FORCE_ANSWER_TO_QUESTIONS:=""}" ]]; then
     RESPONSE=${FORCE_ANSWER_TO_QUESTIONS}
     echo
     echo "Forcing response '${FORCE_ANSWER_TO_QUESTIONS}' to ${1}."
@@ -31,13 +31,13 @@ if [[ "${FORCE_ANSWER_TO_QUESTIONS:=""}" != "" ]]; then
     esac
 else
     echo
-    echo "Please confirm ${1} images. Are you sure? [y/N/q]"
+    echo "Please confirm ${1}. Are you sure? [y/N/q]"
     read -r RESPONSE
 fi
 
 case "${RESPONSE}" in
     [yY][eE][sS]|[yY])
-        echo "The answer is 'yes'. Attempting to ${1} images. This can take some time !"
+        echo "The answer is 'yes'. Attempting to ${1}."
         exit 0
         ;;
     [qQ][uU][iI][tT]|[qQ])
@@ -45,7 +45,7 @@ case "${RESPONSE}" in
         exit 2
         ;;
     *)
-        echo "The answer is 'no'. Skipping attempt to ${1} images."
+        echo "The answer is 'no'. Skipping attempt to ${1}."
         exit 1
         ;;
 esac
diff --git a/docs/build.sh b/docs/build.sh
index 087741168a720..e35970d9245f3 100755
--- a/docs/build.sh
+++ b/docs/build.sh
@@ -54,7 +54,6 @@ echo "Removed content of the _build and _api folders"
 set +e
 # shellcheck disable=SC2063
 NUM_INCORRECT_USE_LITERALINCLUDE=$(grep -inR --include \*.rst 'literalinclude::.\+example_dags' . | \
-    tee /dev/tty |
     wc -l |\
     tr -d '[:space:]')
 set -e
@@ -134,14 +133,13 @@ fi
 
 
 SUCCEED_LINE=$(make html |\
-    tee /dev/tty |\
     grep 'build succeeded' |\
     head -1)
 
 NUM_CURRENT_WARNINGS=$(echo "${SUCCEED_LINE}" |\
     sed -E 's/build succeeded, ([0-9]+) warnings?\./\1/g')
 
-if [[  -f /.dockerenv ]]; then
+if [[  -f /.dockerenv && -n "${HOST_USER_ID:=}" && -n "${HOST_GROUP_ID:=}" ]]; then
     # We are inside the container which means that we should fix back the permissions of the
     # _build and _api folder files, so that they can be accessed by the host user
     # The _api folder should be deleted by then but just in case we should change the ownership
diff --git a/hooks/build b/hooks/build
index 5923b545c709d..ec4b75a9f6e0b 100755
--- a/hooks/build
+++ b/hooks/build
@@ -23,440 +23,54 @@
 set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-
 echo "My dir: ${MY_DIR}"
 
-AIRFLOW_SOURCES=$(cd "${MY_DIR}/.." || exit 1; pwd)
-cd "${AIRFLOW_SOURCES}"
-
-# shellcheck source=common/_default_branch.sh
-source "${AIRFLOW_SOURCES}/common/_default_branch.sh"
-# shellcheck source=common/_files_for_rebuild_check.sh
-source "${AIRFLOW_SOURCES}/common/_files_for_rebuild_check.sh"
-
-echo
-echo "Airflow root directory: ${AIRFLOW_SOURCES}"
-echo
+AIRFLOW_SOURCES="$(cd "${MY_DIR}/.." && pwd)"
+export AIRFLOW_SOURCES
 
-BUILD_CACHE_DIR="${AIRFLOW_SOURCES}/.build"
-mkdir -pv "${BUILD_CACHE_DIR}"
+# The builds in DockerHub are executed within container so we should skip the check
+export SKIP_DOCKER_CONTAINER_CHECK="true"
+export FORCE_ANSWER_TO_QUESTIONS="yes"
 
-date
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/../scripts/ci/utils/_include_all.sh"
 
-BUILD_START_TIME=$(date +%s)
-LAST_STEP_START_TIME=${BUILD_START_TIME}
-LAST_STEP_NAME=""
-STEP_STARTED="false"
-PYTHON_VERSION_FOR_DEFAULT_IMAGE=3.6
-VERBOSE=${VERBOSE:="false"}
-
-if [[ ${VERBOSE} == "true" ]]; then
-    set -x
+if [[ -z ${DOCKER_TAG:=} ]]; then
+   echo
+   echo "Error! Missing DOCKER_TAG environment variable"
+   echo "Please specify DOCKER_TAG variable following the patter ${DEFAULT_BRANCH}-pythonX.Y[-ci]"
+   echo
+   exit 1
 fi
 
-function end_step {
-    if [[ "${STEP_STARTED}" != "true" ]]; then
-        return
-    fi
-    LAST_STEP_END_TIME=$(date +%s)
-    echo
-    echo "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
-    echo "                     Finishing step: ${LAST_STEP_NAME}"
-    echo "                     Date: $(date)"
-    echo "                     Step time in s : $((LAST_STEP_END_TIME-LAST_STEP_START_TIME))"
-    echo "                     Total time in s: $((LAST_STEP_END_TIME-BUILD_START_TIME))"
-    echo "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
-    echo
-    STEP_STARTED="false"
-}
 
-function start_step {
-    end_step
-    LAST_STEP_NAME="${1}"
-    LAST_STEP_START_TIME=$(date +%s)
-    STEP_STARTED="true"
-    echo
-    echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
-    echo "                     Starting step: ${LAST_STEP_NAME}"
-    echo "                     Date: $(date)"
-    echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
-    echo
-}
+[[ ${DOCKER_TAG:=} =~ ${DEFAULT_BRANCH}-python([0-9.]*) ]] && export PYTHON_VERSION=${BASH_REMATCH[1]}
 
-function add_image_to_push {
-    IMAGE=$1
-    IMAGES_BUILT="${IMAGES_BUILT} ${IMAGE}"
-    echo
-    echo "Adding TAG ${IMAGE} to push"
+if [[ -z ${PYTHON_VERSION:=} ]]; then
     echo
+    echo "Error! Wrong DOCKER_TAG"
+    echo "The tag '${DOCKER_TAG}' should follow the pattern ${DEFAULT_BRANCH}-pythonX.Y[-ci]"
     echo
-    echo "List of tags to push now: '${IMAGES_BUILT}'"
-    echo
-}
-
-function build_python_image {
-    NAME="${1}"
-    MY_IMAGE_TAG="${2}"
-    TARGET_IMAGE="${3}"
-    AIRFLOW_EXTRAS="${4:-all}"
-    HOME="${6:-/home/airflow}"
-
-    echo "Build ${NAME} image: ${MY_IMAGE_TAG}"
-    echo "Python base image: ${PYTHON_BASE_IMAGE}"
-
-    set +u
-    set -x
-    docker build \
-        --build-arg PYTHON_BASE_IMAGE="${PYTHON_BASE_IMAGE}" \
-        --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
-        --build-arg AIRFLOW_EXTRAS="${AIRFLOW_EXTRAS}" \
-        --build-arg AIRFLOW_BRANCH="${AIRFLOW_CONTAINER_BRANCH_NAME}" \
-        --build-arg AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD="${AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD}" \
-        --build-arg HOME="${HOME}" \
-        "${DOCKER_CACHE_DIRECTIVE_CI[@]}" \
-        -t "${MY_IMAGE_TAG}" \
-        --target "${TARGET_IMAGE}" \
-        .
-
-    add_image_to_push "${MY_IMAGE_TAG}"
-    set +x
-    set -u
-}
-
-start_step "Setting variables"
-
-# shellcheck source=common/_autodetect_variables.sh
-. "${AIRFLOW_SOURCES}/common/_autodetect_variables.sh"
-
-echo
-echo "Travis event type: ${TRAVIS_EVENT_TYPE:=}"
-echo
-
-# In case of CRON jobs on Travis we run builds without cache
-if [[ "${TRAVIS_EVENT_TYPE:=}" == "cron" ]]; then
-    echo
-    echo "Disabling cache for CRON jobs"
-    echo
-    AIRFLOW_CONTAINER_USE_NO_CACHE=${AIRFLOW_CONTAINER_USE_NO_CACHE:="true"}
-fi
-
-# You can set AIRFLOW_CONTAINER_USE_NO_CACHE to true if you want to use standard Docker cache during build
-# This way you can test building everything from the scratch
-AIRFLOW_CONTAINER_USE_NO_CACHE=${AIRFLOW_CONTAINER_USE_NO_CACHE:="false"}
-
-# If cache is not used, there is no point in pulling images for cache
-if [[ "${AIRFLOW_CONTAINER_USE_NO_CACHE:=}" == "true" ]]; then
-    echo
-    echo "Pulling images is disabled because cache is not used"
-    echo
-    AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE="false"
-fi
-
-
-# You can set AIRFLOW_CONTAINER_USE_DOCKER_CACHE to false if you do not want to use pulled images
-# as cache during build
-# This way you can test building from the scratch
-AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE=${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE:="true"}
-
-pwd
-# Determine version of the Airflow from version.py
-AIRFLOW_VERSION=$(cat airflow/version.py - << EOF | python
-print(version.replace("+",""))
-EOF
-)
-export AIRFLOW_VERSION
-
-# Check if we are running in the CI environment
-CI=${CI:="false"}
-
-if [[ "${CI}" == "true" ]]; then
-    NON_CI="false"
-else
-    NON_CI="true"
-fi
-
-# Extras used to build cache and CI image
-AIRFLOW_CI_EXTRAS=${AIRFLOW_CI_EXTRAS:="devel_ci"}
-
-# Whether this is a release build
-AIRFLOW_RELEASE_BUILD=${AIRFLOW_RELEASE_BUILD:="false"}
-
-echo
-echo "Airflow ${AIRFLOW_VERSION} Python: ${PYTHON_VERSION}."
-echo
-
-# Whether to push images after build
-# This is set to false on CI builds
-export AIRFLOW_CONTAINER_PUSH_IMAGES=${AIRFLOW_CONTAINER_PUSH_IMAGES:=${NON_CI}}
-
-# Whether to force pull images to populate cache
-export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES=${AIRFLOW_CONTAINER_FORCE_PULL_IMAGES:="false"}
-
-# In CI environment (and local force) we skip pulling latest python image
-export AIRFLOW_CONTAINER_SKIP_LATEST_PYTHON_PULL=${AIRFLOW_CONTAINER_SKIP_LATEST_PYTHON_PULL:=${CI}}
-
-# Skips downloading and building the CI image, full CI-enabled image of airflow
-# This is set to true for pre-tests which do not need the whole CI image
-export AIRFLOW_CONTAINER_SKIP_CI_IMAGE=${AIRFLOW_CONTAINER_SKIP_CI_IMAGE:="false"}
-echo "Skip CI image: ${AIRFLOW_CONTAINER_SKIP_CI_IMAGE}"
-
-# Fixes permissions for git-checked out files. This is needed to have consistent build cache across
-# Dockerhub, TravisCI and locally checked out code
-export AIRFLOW_FIX_PERMISSIONS=${AIRFLOW_FIX_PERMISSIONS:="all"}
-echo "Fixing permissions: ${AIRFLOW_FIX_PERMISSIONS}"
-
-export AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD=${AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD:="true"}
-echo "The build optimised for CI: ${AIRFLOW_CONTAINER_CI_OPTIMISED_BUILD}"
-
-# Base python image for the build
-export PYTHON_BASE_IMAGE=python:${PYTHON_VERSION}-slim-stretch
-
-if [[ "${AIRFLOW_RELEASE_BUILD}" == "true" ]]; then
-    export AIRFLOW_CI_IMAGE="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_VERSION}-python${PYTHON_VERSION}-ci"
-    export AIRFLOW_CI_IMAGE_DEFAULT="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_VERSION}-ci"
-    export AIRFLOW_CI_IMAGE_LATEST="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:latest-python${PYTHON_VERSION}-${TAG_PREFIX}-ci"
-    export AIRFLOW_CI_IMAGE_LATEST_DEFAULT="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:latest-${TAG_PREFIX}-ci"
+    exit 1
 else
-    export AIRFLOW_CI_IMAGE="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${TAG_PREFIX}-python${PYTHON_VERSION}-ci"
-    export AIRFLOW_CI_IMAGE_DEFAULT="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${TAG_PREFIX}-ci"
-fi
-# In the future we can enable buildkit.
-# It's experimental now and cache does not work out of the box with buildkit in Docker 18.09.2, buildkit 0.3.3
-# It is fixed in upcoming buildkit 0.4.0.
-# Buildkit will make build faster (including parallel builds of multi-stage builds).
-# It will also help with simpler skipping of unused images.
-export DOCKER_BUILDKIT=${DOCKER_BUILDKIT:=0}
-
-# List of images to push at the end of the build
-IMAGES_BUILT=""
-
-end_step
-
-AIRFLOW_CONTAINER_CLEANUP_IMAGES=${AIRFLOW_CONTAINER_CLEANUP_IMAGES:="false"}
-if [[ "${AIRFLOW_CONTAINER_CLEANUP_IMAGES}" == "true" ]]; then
-    echo
-    "${AIRFLOW_SOURCES}/confirm" "Removing all local images for that build."
-    echo
-    start_step "Removing images"
-    docker rmi "${PYTHON_BASE_IMAGE}" || true
-    docker rmi "${AIRFLOW_CI_IMAGE}" || true
-    echo
-    echo "###################################################################"
-    echo "NOTE!! Removed Airflow images for Python version ${PYTHON_VERSION}."
-    echo "       But the disk space in docker will be reclaimed only after"
-    echo "       running 'docker system prune' command."
-    echo "###################################################################"
-    echo
-    end_step
-    exit 0
-fi
-
-start_step "Populating cache"
-
-DOCKER_CACHE_DIRECTIVE_CI=()
-
-if [[ "${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE}" == "true" ]]; then
-    echo
-    echo "Pulling images to populate cache"
-    echo
-    echo
-    if [[ "${AIRFLOW_CONTAINER_FORCE_PULL_IMAGES}" == "true" ]]; then
-        if [[ ${AIRFLOW_CONTAINER_SKIP_LATEST_PYTHON_PULL} == "true" ]]; then
-            echo
-            echo "Skip force-pulling the base images."
-            echo
-        else
-            set -x
-            echo
-            echo "Force pull base python image."
-            echo
-            docker pull "${PYTHON_BASE_IMAGE}"
-            echo
-            set +x
-        fi
-    fi
-    IMAGES_TO_PULL=""
-    if [[ "${AIRFLOW_CONTAINER_SKIP_CI_IMAGE}" != "true" ]]; then
-        echo "Downloading the CI Airflow image"
-        IMAGES_TO_PULL="${IMAGES_TO_PULL} ${AIRFLOW_CI_IMAGE}"
-    fi
-
-    DOCKER_CACHE_DIRECTIVE_CI=()
-    if [[ ${IMAGES_TO_PULL} == "" ]]; then
-        echo
-        echo "Skipping building of all images."
-        echo
-    else
-        for IMAGE in ${IMAGES_TO_PULL}
-        do
-            echo
-            echo "Checking whether image ${IMAGE} needs to be pulled."
-            echo
-            PULL_IMAGE="false"
-            if [[ "${AIRFLOW_CONTAINER_FORCE_PULL_IMAGES}" == "true" ]]; then
-                echo
-                echo "Pulling images is forced. Pulling ${IMAGE}"
-                echo
-                PULL_IMAGE="true"
-            else
-                IMAGE_HASH=$(docker images -q "${IMAGE}" 2> /dev/null)
-                if [[ "${IMAGE_HASH}" == "" ]]; then
-                    echo
-                    echo "No image ${IMAGE} locally available. Pulling for the first time."
-                    echo
-                    PULL_IMAGE="true"
-                else
-                    echo
-                    echo "Image ${IMAGE} is in local registry (${IMAGE_HASH}). Not pulling it!"
-                    echo
-                    PULL_IMAGE="false"
-                fi
-            fi
-            if [[ "${PULL_IMAGE}" == "true" ]]; then
-                echo
-                set -x
-                docker pull "${IMAGE}" || true
-                set +x
-                echo
-            fi
-            if [[ "${IMAGE}" == "${AIRFLOW_CI_IMAGE}" ]]; then
-                DOCKER_CACHE_DIRECTIVE_CI+=("--cache-from" "${IMAGE}")
-            else
-                echo
-                echo "Don't know how to set cache directive for ${IMAGE}. Exiting"
-                echo
-                exit 1
-            fi
-        done
-    fi
-fi
-
-start_step "Setting cache options"
-
-if [[ "${AIRFLOW_CONTAINER_USE_NO_CACHE}" == "true" ]]; then
-    DOCKER_CACHE_DIRECTIVE_CI+=("--no-cache")
-    echo
-    echo "Skip cache for builds. Everything will be rebuilt from scratch."
-    echo
-    echo "Cache directives used: "
-    echo "CI build: ${DOCKER_CACHE_DIRECTIVE_CI[*]}"
-    echo
-elif [[ "${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE}" == "true" ]]; then
-    echo
-    echo "This build uses Docker cache from pulled images"
-    echo "Cache directives used: "
-    set +u
-    echo "CI build: ${DOCKER_CACHE_DIRECTIVE_CI[*]}"
-    set -u
     echo
-else
-    DOCKER_CACHE_DIRECTIVE_CI=()
-    echo
-    echo "Use default cache from locally built images."
-    echo
-    echo "Cache directives used: "
-    set +u
-    echo "CI build: ${DOCKER_CACHE_DIRECTIVE_CI[*]}"
-    set -u
+    echo "Detected PYTHON_VERSION=${PYTHON_VERSION}"
     echo
 fi
 
-start_step "Creating deployment directory"
-
-STAT_BIN=stat
-if [[ "${OSTYPE}" == "darwin"* ]]; then
-    STAT_BIN=gstat
-fi
-# Build id identifying the build uniquely
-BUILD_ID=${BUILD_ID:="local"}
-
-
-# directory where "deployment" artifacts should be placed
-DEPLOY_DIR="${AIRFLOW_SOURCES}/dist/${AIRFLOW_CONTAINER_BRANCH_NAME}/$(date "+%Y-%m-%d")/${BUILD_ID}/${PYTHON_VERSION}"
-
-mkdir -pv "${DEPLOY_DIR}"
-
-#
-# Fixing permissions for all important files that are going to be added to Docker context
-# This is necessary, because there are different default umask settings on different *NIX
-# In case of some systems (especially in the CI environments) there is default +w group permission
-# set automatically via UMASK when git checkout is performed.
-#    https://unix.stackexchange.com/questions/315121/why-is-the-default-umask-002-or-022-in-many-unix-systems-seems-insecure-by-defa
-# Unfortunately default setting in git is to use UMASK by default:
-#    https://git-scm.com/docs/git-config/1.6.3.1#git-config-coresharedRepository
-# This messes around with Docker context invalidation because the same files have different permissions
-# and effectively different hash used for context validation calculation.
-#
-# We fix it by removing write permissions for other/group for all files that are in the Docker context.
-#
-STAT_BIN=stat
-if [[ "${OSTYPE}" == "darwin"* ]]; then
-    STAT_BIN=gstat
-fi
+script_start
 
-for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"; do
-    ACCESS_RIGHTS=$("${STAT_BIN}" -c "%A" "${AIRFLOW_SOURCES}/${FILE}" || echo "--------")
-    # check if the file is group/other writeable
-    if [[ "${ACCESS_RIGHTS:5:1}" != "-" || "${ACCESS_RIGHTS:8:1}" != "-" ]]; then
-        if [[ "${VERBOSE_FIX_FILE:="false"}" == "true" ]]; then
-            "${STAT_BIN}" --printf "%a %A %F \t%s \t->    " "${AIRFLOW_SOURCES}/${FILE}"
-        fi
-        chmod og-w "${AIRFLOW_SOURCES}/${FILE}"
-        if [[ "${VERBOSE_FIX_FILE:="false"}" == "true" ]]; then
-            "${STAT_BIN}" --printf "%a %A %F \t%s \t%n\n" "${AIRFLOW_SOURCES}/${FILE}"
-        fi
-    fi
-done
+initialize_environment
 
-echo "Group/other write access removed for ${FILES_FOR_REBUILD_CHECK[*]}"
-echo
-echo
+prepare_build
 
-if [[ "${AIRFLOW_CONTAINER_SKIP_CI_IMAGE}" == "true" ]]; then
-    echo "Skip building the CI full Airflow image"
-else
-    start_step "Build Airflow CI full image"
-    build_python_image "Airflow CI" \
-                "${AIRFLOW_CI_IMAGE}" \
-                "main" \
-                "devel_ci" \
-                "root" \
-                "/root"
+set_image_variables
 
-    if [[ "${PYTHON_VERSION_FOR_DEFAULT_IMAGE}" == "${PYTHON_VERSION}" ]]; then
-        docker tag "${AIRFLOW_CI_IMAGE}" "${AIRFLOW_CI_IMAGE_DEFAULT}"
-        add_image_to_push "${AIRFLOW_CI_IMAGE_DEFAULT}"
-    fi
-    if [[ "${AIRFLOW_RELEASE_BUILD}" == "true" ]]; then
-        docker tag "${AIRFLOW_CI_IMAGE}" "${AIRFLOW_CI_IMAGE_LATEST}"
-        add_image_to_push "${AIRFLOW_CI_IMAGE_LATEST}"
-        if [[ "${PYTHON_VERSION_FOR_DEFAULT_IMAGE}" == "${PYTHON_VERSION}" ]]; then
-            docker tag "${AIRFLOW_CI_IMAGE}" "${AIRFLOW_CI_IMAGE_LATEST_DEFAULT}"
-            add_image_to_push "${AIRFLOW_CI_IMAGE_LATEST_DEFAULT}"
-        fi
-    fi
-fi
+rm -rf "${BUILD_CACHE_DIR}"
 
-start_step "Pushing images"
+pull_base_python_image
 
-if [[ "${AIRFLOW_CONTAINER_PUSH_IMAGES}" != "false" ]]; then
-    echo
-    echo "Pushing images: ${IMAGES_BUILT}"
-    echo
-    for IMAGE in ${IMAGES_BUILT}
-    do
-        echo "Pushing image '${IMAGE}'"
-        docker push ${IMAGE}
-    done
-else
-    echo
-    echo "Skip pushing images."
-    echo "Images built: ${IMAGES_BUILT}"
-    echo
+if [[ ${DOCKER_TAG} == *-ci ]]; then
+    rebuild_ci_image_if_needed
+    push_ci_image
 fi
-
-end_step
-
-echo
-echo "Build finished"
-echo
diff --git a/hooks/push b/hooks/push
deleted file mode 100755
index 91cf0969ea6f3..0000000000000
--- a/hooks/push
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# We do not push in the push step because we are building multiple images in the build step
-# and it is difficult to pass list of the built images from the build to push phase
-set -euo pipefail
-
-MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-
-echo "My dir: ${MY_DIR}"
-
-
-echo
-echo "Skip pushing the image. All images were built and pushed in the build hook already!"
-echo
diff --git a/scripts/ci/_utils.sh b/scripts/ci/_utils.sh
deleted file mode 100644
index 71d3ab52d30ae..0000000000000
--- a/scripts/ci/_utils.sh
+++ /dev/null
@@ -1,916 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-# Assume all the scripts are sourcing the _utils.sh from the scripts/ci directory
-# and MY_DIR variable is set to this directory. It can be overridden however
-
-if [[ ${VERBOSE:=} == "true" ]]; then
-    set -x
-else
-    set +x
-fi
-
-AIRFLOW_SOURCES=${AIRFLOW_SOURCES:=$(cd "${MY_DIR}/../../" && pwd)}
-export AIRFLOW_SOURCES
-
-BUILD_CACHE_DIR="${AIRFLOW_SOURCES}/.build"
-export BUILD_CACHE_DIR
-
-LAST_FORCE_ANSWER_FILE="${BUILD_CACHE_DIR}/last_force_answer.sh"
-
-IMAGES_TO_CHECK=("CI")
-export IMAGES_TO_CHECK
-
-mkdir -p "${AIRFLOW_SOURCES}/.mypy_cache"
-mkdir -p "${AIRFLOW_SOURCES}/logs"
-mkdir -p "${AIRFLOW_SOURCES}/tmp"
-
-# shellcheck source=common/_autodetect_variables.sh
-. "${AIRFLOW_SOURCES}/common/_autodetect_variables.sh"
-# shellcheck source=common/_files_for_rebuild_check.sh
-. "${AIRFLOW_SOURCES}/common/_files_for_rebuild_check.sh"
-
-# Default branch name for triggered builds is the one configured in default branch
-export AIRFLOW_CONTAINER_BRANCH_NAME=${AIRFLOW_CONTAINER_BRANCH_NAME:=${DEFAULT_BRANCH}}
-
-# Default port numbers for forwarded ports
-export WEBSERVER_HOST_PORT=${WEBSERVER_HOST_PORT:="28080"}
-export POSTGRES_HOST_PORT=${POSTGRES_HOST_PORT:="25433"}
-export MYSQL_HOST_PORT=${MYSQL_HOST_PORT:="23306"}
-
-# Do not push images from here by default (push them directly from the build script on Dockerhub)
-export AIRFLOW_CONTAINER_PUSH_IMAGES=${AIRFLOW_CONTAINER_PUSH_IMAGES:="false"}
-
-# Disable writing .pyc files - slightly slower imports but not messing around when switching
-# Python version and avoids problems with root-owned .pyc files in host
-export PYTHONDONTWRITEBYTECODE=${PYTHONDONTWRITEBYTECODE:="true"}
-
-#
-# Sets mounting of host volumes to container for static checks
-# unless AIRFLOW_MOUNT_HOST_VOLUMES_FOR_STATIC_CHECKS is not true
-#
-# Note that this cannot be function because we need the AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS array variable
-#
-AIRFLOW_MOUNT_HOST_VOLUMES_FOR_STATIC_CHECKS=${AIRFLOW_MOUNT_HOST_VOLUMES_FOR_STATIC_CHECKS:="true"}
-
-# If this variable is set, we mount the whole sources directory to the host rather than
-# selected volumes
-AIRFLOW_MOUNT_SOURCE_DIR_FOR_STATIC_CHECKS=${AIRFLOW_MOUNT_SOURCE_DIR_FOR_STATIC_CHECKS="false"}
-
-function print_info() {
-    if [[ ${AIRFLOW_CI_SILENT:="false"} != "true" || ${VERBOSE:="false"} == "true" ]]; then
-        echo "$@"
-    fi
-}
-
-declare -a AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS
-if [[ ${AIRFLOW_MOUNT_SOURCE_DIR_FOR_STATIC_CHECKS} == "true" ]]; then
-    print_info
-    print_info "Mount whole sourcce directory for static checks"
-    print_info
-    AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS=( \
-      "-v" "${AIRFLOW_SOURCES}:/opt/airflow" \
-      "--env" "PYTHONDONTWRITEBYTECODE" \
-    )
-elif [[ ${AIRFLOW_MOUNT_HOST_VOLUMES_FOR_STATIC_CHECKS} == "true" ]]; then
-    print_info
-    print_info "Mounting necessary host volumes to Docker"
-    print_info
-    AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS=( \
-      "-v" "${AIRFLOW_SOURCES}/airflow:/opt/airflow/airflow:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.mypy_cache:/opt/airflow/.mypy_cache:cached" \
-      "-v" "${AIRFLOW_SOURCES}/dev:/opt/airflow/dev:cached" \
-      "-v" "${AIRFLOW_SOURCES}/docs:/opt/airflow/docs:cached" \
-      "-v" "${AIRFLOW_SOURCES}/scripts:/opt/airflow/scripts:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.bash_history:/root/.bash_history:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.bash_aliases:/root/.bash_aliases:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.inputrc:/root/.inputrc:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.bash_completion.d:/root/.bash_completion.d:cached" \
-      "-v" "${AIRFLOW_SOURCES}/tmp:/opt/airflow/tmp:cached" \
-      "-v" "${AIRFLOW_SOURCES}/tests:/opt/airflow/tests:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.flake8:/opt/airflow/.flake8:cached" \
-      "-v" "${AIRFLOW_SOURCES}/pylintrc:/opt/airflow/pylintrc:cached" \
-      "-v" "${AIRFLOW_SOURCES}/setup.cfg:/opt/airflow/setup.cfg:cached" \
-      "-v" "${AIRFLOW_SOURCES}/setup.py:/opt/airflow/setup.py:cached" \
-      "-v" "${AIRFLOW_SOURCES}/.rat-excludes:/opt/airflow/.rat-excludes:cached" \
-      "-v" "${AIRFLOW_SOURCES}/logs:/opt/airflow/logs:cached" \
-      "-v" "${AIRFLOW_SOURCES}/logs:/root/logs:cached" \
-      "-v" "${AIRFLOW_SOURCES}/files:/files:cached" \
-      "-v" "${AIRFLOW_SOURCES}/tmp:/opt/airflow/tmp:cached" \
-      "--env" "PYTHONDONTWRITEBYTECODE" \
-    )
-else
-    print_info
-    print_info "Skip mounting host volumes to Docker"
-    print_info
-    AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS=( \
-        "--env" "PYTHONDONTWRITEBYTECODE" \
-    )
-fi
-
-export AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS
-
-#
-# Creates cache directory where we will keep temporary files needed for the build
-#
-# This directory will be automatically deleted when the script is killed or exists (via trap)
-# Unless SKIP_CACHE_DELETION variable is set. You can set this variable and then see
-# the output/files generated by the scripts in this directory.
-#
-# Most useful is out.log file in this directory storing verbose output of the scripts.
-#
-function create_cache_directory() {
-    CACHE_TMP_FILE_DIR=$(mktemp -d)
-    export CACHE_TMP_FILE_DIR
-
-    if [[ ${SKIP_CACHE_DELETION:=} != "true" ]]; then
-        trap 'rm -rf -- "${CACHE_TMP_FILE_DIR}"' INT TERM HUP
-    fi
-
-    OUTPUT_LOG="${CACHE_TMP_FILE_DIR}/out.log"
-    export OUTPUT_LOG
-}
-
-function remove_cache_directory() {
-    if [[ -z "${CACHE_TMP_FILE_DIR}" ]]; then
-        rm -rf -- "${CACHE_TMP_FILE_DIR}"
-    fi
-}
-
-#
-# Verifies if stored md5sum of the file changed since the last tme ot was checked
-# The md5sum files are stored in .build directory - you can delete this directory
-# If you want to rebuild everything from the scratch
-#
-function check_file_md5sum {
-    local FILE="${1}"
-    local MD5SUM
-    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${THE_IMAGE_TYPE}"
-    mkdir -pv "${MD5SUM_CACHE_DIR}"
-    MD5SUM=$(md5sum "${FILE}")
-    local MD5SUM_FILE
-    MD5SUM_FILE="${MD5SUM_CACHE_DIR}"/$(basename "${FILE}").md5sum
-    local MD5SUM_FILE_NEW
-    MD5SUM_FILE_NEW=${CACHE_TMP_FILE_DIR}/$(basename "${FILE}").md5sum.new
-    echo "${MD5SUM}" > "${MD5SUM_FILE_NEW}"
-    local RET_CODE=0
-    if [[ ! -f "${MD5SUM_FILE}" ]]; then
-        print_info "Missing md5sum for ${FILE#${AIRFLOW_SOURCES}} (${MD5SUM_FILE#${AIRFLOW_SOURCES}})"
-        RET_CODE=1
-    else
-        diff "${MD5SUM_FILE_NEW}" "${MD5SUM_FILE}" >/dev/null
-        RES=$?
-        if [[ "${RES}" != "0" ]]; then
-            print_info "The md5sum changed for ${FILE}"
-            RET_CODE=1
-        fi
-    fi
-    return ${RET_CODE}
-}
-
-#
-# Moves md5sum file from it's temporary location in CACHE_TMP_FILE_DIR to
-# BUILD_CACHE_DIR - thus updating stored MD5 sum fo the file
-#
-function move_file_md5sum {
-    local FILE="${1}"
-    local MD5SUM_FILE
-    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${THE_IMAGE_TYPE}"
-    mkdir -pv "${MD5SUM_CACHE_DIR}"
-    MD5SUM_FILE="${MD5SUM_CACHE_DIR}"/$(basename "${FILE}").md5sum
-    local MD5SUM_FILE_NEW
-    MD5SUM_FILE_NEW=${CACHE_TMP_FILE_DIR}/$(basename "${FILE}").md5sum.new
-    if [[ -f "${MD5SUM_FILE_NEW}" ]]; then
-        mv "${MD5SUM_FILE_NEW}" "${MD5SUM_FILE}"
-        print_info "Updated md5sum file ${MD5SUM_FILE} for ${FILE}."
-    fi
-}
-
-#
-# Stores md5sum files for all important files and
-# records that we built the images locally so that next time we use
-# it from the local docker cache rather than pull (unless forced)
-#
-function update_all_md5_files() {
-    print_info
-    print_info "Updating md5sum files"
-    print_info
-    for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"
-    do
-        move_file_md5sum "${AIRFLOW_SOURCES}/${FILE}"
-    done
-    local SUFFIX=""
-    if [[ -n ${PYTHON_VERSION:=""} ]]; then
-        SUFFIX="_${PYTHON_VERSION}"
-    fi
-    mkdir -pv "${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}"
-    touch "${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/.built_${THE_IMAGE_TYPE}${SUFFIX}"
-}
-
-#
-# Checks md5sum of all important files in order to optimise speed of running various operations
-# That mount sources of Airflow to container and require docker image built with latest dependencies.
-# the Docker image will only be marked for rebuilding only in case any of the important files change:
-# * setup.py
-# * setup.cfg
-# * Dockerfile
-# * airflow/version.py
-#
-# This is needed because we want to skip rebuilding of the image when only airflow sources change but
-# Trigger rebuild in case we need to change dependencies (setup.py, setup.cfg, change version of Airflow
-# or the Dockerfile itself changes.
-#
-# Another reason to skip rebuilding Docker is thar currently it takes a bit longer time than simple Docker
-# We need to fix group permissions of files in Docker because different linux build services have
-# different default umask and Docker uses group permissions in checking for cache invalidation.
-#
-# As result of this check - most of the static checks will start pretty much immediately.
-#
-function check_if_docker_build_is_needed() {
-    print_info
-    print_info "Checking if docker image build is needed for ${THE_IMAGE_TYPE} image."
-    print_info
-    local IMAGE_BUILD_NEEDED="false"
-    if [[ ${AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD:=""} == "true" ]]; then
-        print_info "Docker image build is forced for ${THE_IMAGE_TYPE} image"
-        set +e
-        for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"
-        do
-            # Just store md5sum for all files in md5sum.new - do not check if it is different
-            check_file_md5sum "${AIRFLOW_SOURCES}/${FILE}"
-        done
-        set -e
-        IMAGES_TO_REBUILD+=("${THE_IMAGE_TYPE}")
-        export AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="true"
-    else
-        set +e
-        for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"
-        do
-            if ! check_file_md5sum "${AIRFLOW_SOURCES}/${FILE}"; then
-                export AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="true"
-                IMAGE_BUILD_NEEDED=true
-            fi
-        done
-        set -e
-        if [[ ${IMAGE_BUILD_NEEDED} == "true" ]]; then
-            IMAGES_TO_REBUILD+=("${THE_IMAGE_TYPE}")
-            export AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="true"
-            print_info "Docker image build is needed for ${THE_IMAGE_TYPE} image!"
-        else
-            print_info "Docker image build is not needed for ${THE_IMAGE_TYPE} image!"
-        fi
-    fi
-    print_info
-}
-
-#
-# Checks if core utils required in the host system are installed and explain what needs to be done if not
-#
-function check_if_coreutils_installed() {
-    set +e
-    getopt -T >/dev/null
-    GETOPT_RETVAL=$?
-
-    if [[ $(uname -s) == 'Darwin' ]] ; then
-        command -v gstat >/dev/null
-        STAT_PRESENT=$?
-    else
-        command -v stat >/dev/null
-        STAT_PRESENT=$?
-    fi
-
-    command -v md5sum >/dev/null
-    MD5SUM_PRESENT=$?
-
-    set -e
-
-    CMDNAME="$(basename -- "$0")"
-
-    ####################  Parsing options/arguments
-    if [[ ${GETOPT_RETVAL} != 4 || "${STAT_PRESENT}" != "0" || "${MD5SUM_PRESENT}" != "0" ]]; then
-        print_info
-        if [[ $(uname -s) == 'Darwin' ]] ; then
-            echo >&2 "You are running ${CMDNAME} in OSX environment"
-            echo >&2 "And you need to install gnu commands"
-            echo >&2
-            echo >&2 "Run 'brew install gnu-getopt coreutils'"
-            echo >&2
-            echo >&2 "Then link the gnu-getopt to become default as suggested by brew."
-            echo >&2
-            echo >&2 "If you use bash, you should run this command:"
-            echo >&2
-            echo >&2 "echo 'export PATH=\"/usr/local/opt/gnu-getopt/bin:\$PATH\"' >> ~/.bash_profile"
-            echo >&2 ". ~/.bash_profile"
-            echo >&2
-            echo >&2 "If you use zsh, you should run this command:"
-            echo >&2
-            echo >&2 "echo 'export PATH=\"/usr/local/opt/gnu-getopt/bin:\$PATH\"' >> ~/.zprofile"
-            echo >&2 ". ~/.zprofile"
-            echo >&2
-            echo >&2 "Login and logout afterwards !!"
-            echo >&2
-            echo >&2 "After re-login, your PATH variable should start with \"/usr/local/opt/gnu-getopt/bin\""
-            echo >&2 "Your current path is ${PATH}"
-            echo >&2
-        else
-            echo >&2 "You do not have necessary tools in your path (getopt, stat, md5sum)."
-            echo >&2 "Please install latest/GNU version of getopt and coreutils."
-            echo >&2 "This can usually be done with 'apt install util-linux coreutils'"
-        fi
-        print_info
-        exit 1
-    fi
-}
-
-#
-# Asserts that we are not inside of the container
-#
-function assert_not_in_container() {
-    if [[ -f /.dockerenv ]]; then
-        echo >&2
-        echo >&2 "You are inside the Airflow docker container!"
-        echo >&2 "You should only run this script from the host."
-        echo >&2 "Learn more about how we develop and test airflow in:"
-        echo >&2 "https://github.com/apache/airflow/blob/master/CONTRIBUTING.md"
-        echo >&2
-        exit 1
-    fi
-}
-
-function forget_last_answer() {
-    # Removes the "Forced answer" (yes/no/quit) given previously, unles you specifically want to remember it.
-    #
-    # This is the default behaviour of all rebuild scripts to ask independently whether you want to
-    # rebuild the image or not. Sometimes however we want to remember answer previously given. For
-    # example if you answered "no" to rebuild the image, the assumption is that you do not
-    # want to rebuild image also for other rebuilds in the same pre-commit execution.
-    #
-    # All the pre-commit checks therefore have `export REMEMBER_LAST_ANSWER="true"` set
-    # So that in case they are run in a sequence of commits they will not rebuild. Similarly if your most
-    # recent answer was "no" and you run `pre-commit run mypy` (for example) it will also reuse the
-    # "no" answer given previously. This happens until you run any of the breeze commands or run all
-    # precommits `pre-commit run` - then the "LAST_FORCE_ANSWER_FILE" will be removed and you will
-    # be asked again.
-    if [[ ${REMEMBER_LAST_ANSWER:="false"} != "true" ]]; then
-        print_info
-        print_info "Forgetting last answer from ${LAST_FORCE_ANSWER_FILE}:"
-        print_info
-        rm -f "${LAST_FORCE_ANSWER_FILE}"
-    else
-        if [[ -f "${LAST_FORCE_ANSWER_FILE}" ]]; then
-            print_info
-            print_info "Still remember last answer from ${LAST_FORCE_ANSWER_FILE}:"
-            print_info "$(cat "${LAST_FORCE_ANSWER_FILE}")"
-            print_info
-        fi
-    fi
-}
-
-
-function confirm_image_rebuild() {
-    if [[ -f "${LAST_FORCE_ANSWER_FILE}" ]]; then
-        # set variable from last answered response given in the same pre-commit run - so that it can be
-        # set in one pre-commit check (build) and then used in another (pylint/mypy/flake8 etc).
-        # shellcheck disable=SC1090
-        source "${LAST_FORCE_ANSWER_FILE}"
-    fi
-    set +e
-    if [[ ${CI:="false"} == "true" ]]; then
-        print_info
-        print_info "CI environment - forcing ${ACTION} for ${THE_IMAGE_TYPE} image."
-        print_info
-        RES="0"
-    elif [[ -c /dev/tty ]]; then
-        # Make sure to use /dev/tty first rather than stdin/stdout when available - this way confirm
-        # will works also in case of pre-commits (git does not pass stdin/stdout to pre-commit hooks)
-        "${AIRFLOW_SOURCES}/confirm" "${ACTION} ${THE_IMAGE_TYPE}" </dev/tty >/dev/tty
-        RES=$?
-    elif [[ -t 0 ]]; then
-        # Check if this script is run interactively with stdin open and terminal attached
-        "${AIRFLOW_SOURCES}/confirm" "${ACTION} ${THE_IMAGE_TYPE}"
-        RES=$?
-    else
-        # No terminal, no stdin - quitting!
-        RES="2"
-    fi
-    set -e
-    if [[ ${RES} == "1" ]]; then
-        print_info
-        print_info "Skipping ${ACTION} for ${THE_IMAGE_TYPE}"
-        print_info
-        SKIP_REBUILD="true"
-        # Force "no" also to subsequent questions so that if you answer it once, you are not asked
-        # For all other pre-commits and you will continue using the images you already have
-        export FORCE_ANSWER_TO_QUESTIONS="no"
-        echo 'export FORCE_ANSWER_TO_QUESTIONS="no"' > "${LAST_FORCE_ANSWER_FILE}"
-    elif [[ ${RES} == "2" ]]; then
-        echo >&2
-        echo >&2 "ERROR: The image needs ${ACTION} for ${THE_IMAGE_TYPE} - it is outdated. "
-        echo >&2 "   Make sure you build the images bu running run one of:"
-        echo >&2 "         * ./scripts/ci/local_ci_build.sh"
-        echo >&2 "         * ./scripts/ci/local_ci_pull_and_build.sh"
-        echo >&2
-        echo >&2 "   If you run it via pre-commit separately, run 'pre-commit run build' first."
-        echo >&2
-        exit 1
-    else
-        # Force "yes" also to subsequent questions
-        export FORCE_ANSWER_TO_QUESTIONS="yes"
-    fi
-}
-
-function rebuild_image_if_needed() {
-    AIRFLOW_VERSION=$(cat airflow/version.py - << EOF | python
-print(version.replace("+",""))
-EOF
-    )
-    export AIRFLOW_VERSION
-
-    if [[ ${AIRFLOW_CONTAINER_CLEANUP_IMAGES:="false"} == "true" ]]; then
-        print_info
-        print_info "Clean up ${THE_IMAGE_TYPE} image. Just cleanup no pull of images happen."
-        print_info
-        export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="false"
-        export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-    elif [[ -f "${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/.built_${THE_IMAGE_TYPE}_${PYTHON_VERSION}" ]]; then
-        print_info
-        print_info "${THE_IMAGE_TYPE} image already built locally."
-        print_info
-    else
-        print_info
-        print_info "${THE_IMAGE_TYPE} image not built locally: pulling and building"
-        print_info
-        export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="true"
-        export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-    fi
-
-    AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="false"
-    IMAGES_TO_REBUILD=()
-    check_if_docker_build_is_needed
-    if [[ "${AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED}" == "true" ]]; then
-        SKIP_REBUILD="false"
-        if [[ ${AIRFLOW_CONTAINER_CLEANUP_IMAGES} == "true" ]]; then
-            export ACTION="clean"
-        else
-            export ACTION="rebuild"
-        fi
-        if [[ ${CI:=} != "true" && "${FORCE_BUILD:=}" != "true" ]]; then
-            confirm_image_rebuild
-        fi
-        if [[ ${SKIP_REBUILD} != "true" ]]; then
-            print_info
-            print_info "${ACTION} start: ${THE_IMAGE_TYPE} image."
-            print_info
-            # shellcheck source=hooks/build
-            ./hooks/build | tee -a "${OUTPUT_LOG}"
-            update_all_md5_files
-            print_info
-            print_info "${ACTION} completed: ${THE_IMAGE_TYPE} image."
-            print_info
-        fi
-    else
-        print_info
-        print_info "No need to rebuild - none of the important files changed: ${FILES_FOR_REBUILD_CHECK[*]}"
-        print_info
-    fi
-}
-
-#
-# Rebuilds the image for tests if needed.
-#
-function rebuild_ci_image_if_needed() {
-    export AIRFLOW_CONTAINER_SKIP_CI_IMAGE="false"
-
-    export THE_IMAGE_TYPE="CI"
-
-    rebuild_image_if_needed
-
-    export AIRFLOW_CI_IMAGE="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${DEFAULT_BRANCH}-python${PYTHON_VERSION}-ci"
-}
-
-
-#
-# Cleans up the CI image
-#
-function cleanup_ci_image() {
-    export AIRFLOW_CONTAINER_SKIP_CI_IMAGE="false"
-    export AIRFLOW_CONTAINER_CLEANUP_IMAGES="true"
-
-    export THE_IMAGE_TYPE="CI"
-
-    rebuild_image_if_needed
-}
-
-#
-# Starts the script/ If VERBOSE variable is set to true, it enables verbose output of commands executed
-# Also prints some useful diagnostics information at start of the script
-#
-function script_start {
-    print_info
-    print_info "Running $(basename "$0")"
-    print_info
-    print_info "Log is redirected to ${OUTPUT_LOG}"
-    print_info
-    if [[ ${VERBOSE:=} == "true" ]]; then
-        print_info
-        print_info "Variable VERBOSE Set to \"true\""
-        print_info "You will see a lot of output"
-        print_info
-        set -x
-    else
-        print_info "You can increase verbosity by running 'export VERBOSE=\"true\""
-        if [[ ${SKIP_CACHE_DELETION:=} != "true" ]]; then
-            print_info "And skip deleting the output file with 'export SKIP_CACHE_DELETION=\"true\""
-        fi
-        print_info
-        set +x
-    fi
-    START_SCRIPT_TIME=$(date +%s)
-}
-
-#
-#
-# Disables verbosity in the script
-#
-function script_end {
-    if [[ ${VERBOSE:=} == "true" ]]; then
-        set +x
-    fi
-    END_SCRIPT_TIME=$(date +%s)
-    RUN_SCRIPT_TIME=$((END_SCRIPT_TIME-START_SCRIPT_TIME))
-    print_info
-    print_info "Finished the script $(basename "$0")"
-    print_info "It took ${RUN_SCRIPT_TIME} seconds"
-    print_info
-    remove_cache_directory
-}
-
-function go_to_airflow_sources {
-    print_info
-    pushd "${AIRFLOW_SOURCES}" &>/dev/null || exit 1
-    print_info
-    print_info "Running in host in $(pwd)"
-    print_info
-}
-
-#
-# Performs basic sanity checks common for most of the scripts in this directory
-#
-function basic_sanity_checks() {
-    assert_not_in_container
-    go_to_airflow_sources
-    check_if_coreutils_installed
-    create_cache_directory
-    forget_last_answer
-}
-
-
-function run_flake8() {
-    FILES=("$@")
-
-    if [[ "${#FILES[@]}" == "0" ]]; then
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_flake8.sh" \
-            | tee -a "${OUTPUT_LOG}"
-    else
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_flake8.sh" "${FILES[@]}" \
-            | tee -a "${OUTPUT_LOG}"
-    fi
-}
-
-function run_docs() {
-    docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" -t \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/docs/build.sh" \
-            | tee -a "${OUTPUT_LOG}"
-}
-
-function run_check_license() {
-    docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" -t \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_check_licence.sh" \
-            | tee -a "${OUTPUT_LOG}"
-}
-
-function run_mypy() {
-    FILES=("$@")
-    if [[ "${#FILES[@]}" == "0" ]]; then
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_mypy.sh" "airflow" "tests" "docs" \
-            | tee -a "${OUTPUT_LOG}"
-    else
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init" \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_mypy.sh" "${FILES[@]}" \
-            | tee -a "${OUTPUT_LOG}"
-    fi
-}
-
-function run_pylint_main() {
-    FILES=("$@")
-    if [[ "${#FILES[@]}" == "0" ]]; then
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_pylint_main.sh" \
-            | tee -a "${OUTPUT_LOG}"
-    else
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init" \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_pylint_main.sh" "${FILES[@]}" \
-            | tee -a "${OUTPUT_LOG}"
-    fi
-}
-
-
-function run_pylint_tests() {
-    FILES=("$@")
-    if [[ "${#FILES[@]}" == "0" ]]; then
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_pylint_tests.sh" \
-            | tee -a "${OUTPUT_LOG}"
-    else
-        docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
-            --env PYTHONDONTWRITEBYTECODE \
-            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-            --env AIRFLOW_CI_SILENT \
-            --env HOST_USER_ID="$(id -ur)" \
-            --env HOST_GROUP_ID="$(id -gr)" \
-            --rm \
-            "${AIRFLOW_CI_IMAGE}" \
-            "--" "/opt/airflow/scripts/ci/in_container/run_pylint_tests.sh" "${FILES[@]}" \
-            | tee -a "${OUTPUT_LOG}"
-    fi
-}
-
-function run_docker_lint() {
-    FILES=("$@")
-    if [[ "${#FILES[@]}" == "0" ]]; then
-        echo
-        echo "Running docker lint for all Dockerfiles"
-        echo
-        docker run \
-            -v "$(pwd):/root" \
-            -w /root \
-            --rm \
-            hadolint/hadolint /bin/hadolint Dockerfile*
-        echo
-        echo "Docker pylint completed with no errors"
-        echo
-    else
-        echo
-        echo "Running docker lint for $*"
-        echo
-        docker run \
-            -v "$(pwd):/root" \
-            -w /root \
-            --rm \
-            hadolint/hadolint /bin/hadolint "$@"
-        echo
-        echo "Docker pylint completed with no errors"
-        echo
-    fi
-}
-
-function filter_out_files_from_pylint_todo_list() {
-  FILTERED_FILES=()
-  set +e
-  for FILE in "$@"
-  do
-      if [[ ${FILE} == "airflow/migrations/versions/"* ]]; then
-          # Skip all generated migration scripts
-          continue
-      fi
-      if ! grep -x "./${FILE}" <"${AIRFLOW_SOURCES}/scripts/ci/pylint_todo.txt" >/dev/null; then
-          FILTERED_FILES+=("${FILE}")
-      fi
-  done
-  set -e
-  export FILTERED_FILES
-}
-
-function refresh_pylint_todo() {
-    docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-        --entrypoint /opt/airflow/scripts/ci/in_container/refresh_pylint_todo.sh \
-        --env PYTHONDONTWRITEBYTECODE \
-        --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
-        --env AIRFLOW_CI_SILENT \
-        --env HOST_USER_ID="$(id -ur)" \
-        --env HOST_GROUP_ID="$(id -gr)" \
-        "${AIRFLOW_CI_IMAGE}" | tee -a "${OUTPUT_LOG}"
-}
-
-function rebuild_all_images_if_needed_and_confirmed() {
-    AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="false"
-    IMAGES_TO_REBUILD=()
-
-    for THE_IMAGE_TYPE in "${IMAGES_TO_CHECK[@]}"
-    do
-        check_if_docker_build_is_needed
-    done
-
-    if [[ ${AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED} == "true" ]]; then
-        print_info
-        print_info "Docker image build is needed for ${IMAGES_TO_REBUILD[*]}!"
-        print_info
-    else
-        print_info
-        print_info "Docker image build is not needed for any of the image types!"
-        print_info
-    fi
-
-    if [[ "${AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED}" == "true" ]]; then
-        echo
-        echo "Some of your images need to be rebuild because important files (like package list) has changed."
-        echo
-        echo "You have those options:"
-        echo "   * Rebuild the images now by answering 'y' (this might take some time!)"
-        echo "   * Skip rebuilding the images and hope changes are not big (you will be asked again)"
-        echo "   * Quit and manually rebuild the images using"
-        echo "        * scripts/local_ci_build.sh or"
-        echo "        * scripts/local_ci_pull_and_build.sh or"
-        echo
-        export ACTION="rebuild"
-        export THE_IMAGE_TYPE="${IMAGES_TO_REBUILD[*]}"
-
-        SKIP_REBUILD="false"
-        confirm_image_rebuild
-
-        if [[ ${SKIP_REBUILD} != "true" ]]; then
-            rebuild_ci_image_if_needed
-        fi
-    fi
-}
-
-function match_files_regexp() {
-    FILE_MATCHES="false"
-    REGEXP=${1}
-    while (($#))
-    do
-        REGEXP=${1}
-        for FILE in ${CHANGED_FILE_NAMES}
-        do
-          if  [[ ${FILE} =~ ${REGEXP} ]]; then
-             FILE_MATCHES="true"
-          fi
-        done
-        shift
-    done
-    export FILE_MATCHES
-}
-
-function build_image_on_ci() {
-    if [[ "${CI:=}" != "true" ]]; then
-        print_info
-        print_info "Cleaning up docker installation!!!!!!"
-        print_info
-        "${AIRFLOW_SOURCES}/confirm" "Cleaning docker data and rebuilding"
-    fi
-
-    export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="true"
-    export FORCE_BUILD="true"
-    export VERBOSE="${VERBOSE:="false"}"
-
-    # Cleanup docker installation. It should be empty in CI but let's not risk
-    docker system prune --all --force
-    rm -rf "${BUILD_CACHE_DIR}"
-    mkdir -pv "${BUILD_CACHE_DIR}"
-
-    echo
-    echo "Finding changed file names ${TRAVIS_BRANCH}...HEAD"
-    echo
-
-    git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
-    git fetch origin "${TRAVIS_BRANCH}"
-    CHANGED_FILE_NAMES=$(git diff --name-only "remotes/origin/${TRAVIS_BRANCH}...HEAD")
-    echo
-    echo "Changed file names in this commit"
-    echo "${CHANGED_FILE_NAMES}"
-    echo
-
-    if [[ ${TRAVIS_JOB_NAME:=""} == "Tests"*"kubernetes"* ]]; then
-        match_files_regexp 'airflow/kubernetes/.*\.py' 'tests/kubernetes/.*\.py' \
-            'airflow/www/.*\.py' 'airflow/www/.*\.js' 'airflow/www/.*\.html'
-        if [[ ${FILE_MATCHES} == "true" || ${TRAVIS_PULL_REQUEST:=} == "false" ]]; then
-            rebuild_ci_image_if_needed
-        else
-            touch "${BUILD_CACHE_DIR}"/.skip_tests
-        fi
-    elif [[ ${TRAVIS_JOB_NAME:=""} == "Tests"* ]]; then
-        match_files_regexp '.*\.py' 'airflow/www/.*\.py' 'airflow/www/.*\.js' 'airflow/www/.*\.html'
-        if [[ ${FILE_MATCHES} == "true" || ${TRAVIS_PULL_REQUEST:=} == "false" ]]; then
-            rebuild_ci_image_if_needed
-        else
-            touch "${BUILD_CACHE_DIR}"/.skip_tests
-        fi
-    elif [[ ${TRAVIS_JOB_NAME} == "Static"* ]]; then
-        rebuild_ci_image_if_needed
-    elif [[ ${TRAVIS_JOB_NAME} == "Pylint"* ]]; then
-        match_files_regexp '.*\.py'
-        if [[ ${FILE_MATCHES} == "true" || ${TRAVIS_PULL_REQUEST:=} == "false" ]]; then
-            rebuild_ci_image_if_needed
-        else
-            touch "${BUILD_CACHE_DIR}"/.skip_tests
-        fi
-    elif [[ ${TRAVIS_JOB_NAME} == "Build documentation"* ]]; then
-        rebuild_ci_image_if_needed
-    else
-        echo
-        echo "Error! Unexpected Travis job name: ${TRAVIS_JOB_NAME}"
-        echo
-        exit 1
-    fi
-
-    if [[ -f "${BUILD_CACHE_DIR}/.skip_tests" ]]; then
-        echo
-        echo "Skip running tests !!!!"
-        echo
-    fi
-
-    # Disable force pulling forced above
-    unset AIRFLOW_CONTAINER_FORCE_PULL_IMAGES
-    unset FORCE_BUILD
-}
diff --git a/scripts/ci/ci_check_license.sh b/scripts/ci/ci_check_license.sh
index f3d004abfed0f..17557606d275e 100755
--- a/scripts/ci/ci_check_license.sh
+++ b/scripts/ci/ci_check_license.sh
@@ -23,15 +23,22 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 export MOUNT_LOCAL_SOURCES="true"
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+export MOUNT_HOST_VOLUMES="true"
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
 rebuild_ci_image_if_needed
 
 run_check_license
diff --git a/scripts/ci/ci_docs.sh b/scripts/ci/ci_docs.sh
index bf48b50f62d75..b2876134644d6 100755
--- a/scripts/ci/ci_docs.sh
+++ b/scripts/ci/ci_docs.sh
@@ -20,19 +20,28 @@ set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+script_start
 
-basic_sanity_checks
+initialize_environment
 
-script_start
+prepare_build
+
+prepare_run
 
 rebuild_ci_image_if_needed
 
 run_docs
 
+mkdir -p "${AIRFLOW_SOURCES}/airflow/www/static" \
+    && mkdir -p "${AIRFLOW_SOURCES}/docs/build/_html" \
+    && pushd "${AIRFLOW_SOURCES}/airflow/www/static" || exit \
+    && ln -sf ../../../docs/_build/html docs \
+    && popd || exit
+
 script_end
diff --git a/scripts/ci/ci_flake8.sh b/scripts/ci/ci_flake8.sh
index 23118c83a13e3..2e3a232f8ff7a 100755
--- a/scripts/ci/ci_flake8.sh
+++ b/scripts/ci/ci_flake8.sh
@@ -22,15 +22,23 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
-basic_sanity_checks
+# Set default python version
+export PYTHON_VERSION=${STATIC_CHECK_PYTHON_VERSION}
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
 rebuild_ci_image_if_needed
 
 run_flake8 "$@"
diff --git a/scripts/ci/ci_before_install.sh b/scripts/ci/ci_install.sh
similarity index 76%
rename from scripts/ci/ci_before_install.sh
rename to scripts/ci/ci_install.sh
index 2386406415745..bf446ac5c6a71 100755
--- a/scripts/ci/ci_before_install.sh
+++ b/scripts/ci/ci_install.sh
@@ -16,20 +16,27 @@
 # specific language governing permissions and limitations
 # under the License.
 
-set -xeuo pipefail
+set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+export VERBOSE=${VERBOSE:="false"}
 
-basic_sanity_checks
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
+
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
+initialize_environment
+
+prepare_build
+
 build_image_on_ci
 
-KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
+KUBERNETES_MODE=${KUBERNETES_MODE:=""}
 
 mkdir -p "${AIRFLOW_SOURCES}/files"
 
diff --git a/scripts/ci/ci_lint_dockerfile.sh b/scripts/ci/ci_lint_dockerfile.sh
index ebe06de829e8a..ced6e57de9988 100755
--- a/scripts/ci/ci_lint_dockerfile.sh
+++ b/scripts/ci/ci_lint_dockerfile.sh
@@ -22,13 +22,20 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
 run_docker_lint "$@"
 
 script_end
diff --git a/scripts/ci/ci_mypy.sh b/scripts/ci/ci_mypy.sh
index ea656b693d01c..cb7639bee2905 100755
--- a/scripts/ci/ci_mypy.sh
+++ b/scripts/ci/ci_mypy.sh
@@ -22,15 +22,20 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
-
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
 rebuild_ci_image_if_needed
 
 run_mypy "$@"
diff --git a/scripts/ci/ci_pylint_main.sh b/scripts/ci/ci_pylint_main.sh
index 937239812f2af..1e37390bbf286 100755
--- a/scripts/ci/ci_pylint_main.sh
+++ b/scripts/ci/ci_pylint_main.sh
@@ -22,15 +22,24 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
-basic_sanity_checks
+# Set default python version
+export PYTHON_VERSION=${PYTHON_VERSION:=${DEFAULT_PYTHON_VERSION}}
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
+
 rebuild_ci_image_if_needed
 
 if [[ "${#@}" != "0" ]]; then
diff --git a/scripts/ci/ci_pylint_tests.sh b/scripts/ci/ci_pylint_tests.sh
index abf24d286336e..46302d88840ca 100755
--- a/scripts/ci/ci_pylint_tests.sh
+++ b/scripts/ci/ci_pylint_tests.sh
@@ -22,15 +22,23 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
-basic_sanity_checks
+# Set default python version
+export PYTHON_VERSION=${PYTHON_VERSION:=${DEFAULT_PYTHON_VERSION}}
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
 rebuild_ci_image_if_needed
 
 if [[ "${#@}" != "0" ]]; then
diff --git a/scripts/ci/ci_refresh_pylint_todo.sh b/scripts/ci/ci_refresh_pylint_todo.sh
index 46716c3150884..58a308dbaf6f6 100755
--- a/scripts/ci/ci_refresh_pylint_todo.sh
+++ b/scripts/ci/ci_refresh_pylint_todo.sh
@@ -23,13 +23,23 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="false"}
 export FORCE_ANSWER_TO_QUESTIONS=quit
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
+
+# Set default python version
+export PYTHON_VERSION=${STATIC_CHECK_PYTHON_VERSION}
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
 rebuild_ci_image_if_needed
 
 refresh_pylint_todo
diff --git a/scripts/ci/ci_run_airflow_testing.sh b/scripts/ci/ci_run_airflow_testing.sh
index d86086806b7b0..0d16da7727b39 100755
--- a/scripts/ci/ci_run_airflow_testing.sh
+++ b/scripts/ci/ci_run_airflow_testing.sh
@@ -19,32 +19,32 @@
 set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
-
 export VERBOSE=${VERBOSE:="true"}
 
-basic_sanity_checks
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
+
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
+
+PYTHON_VERSION=${PYTHON_VERSION:=${DEFAULT_PYTHON_VERSION}}
 
 script_start
 
-if [[ -f ${BUILD_CACHE_DIR}/.skip_tests ]]; then
-    echo
-    echo "Skipping running tests !!!!!"
-    echo
-    script_end
-    exit
-fi
+initialize_environment
 
+prepare_build
+
+prepare_run
+
+export FORCE_ANSWER_TO_QUESTIONS="yes"
 rebuild_ci_image_if_needed
 
 # Test environment
 export BACKEND=${BACKEND:="sqlite"}
-export ENV=${ENV:="docker"}
-export KUBERNETES_MODE=${KUBERNETES_MODE:="git_mode"}
 
 # Whether necessary for airflow run local sources are mounted to docker
-export MOUNT_LOCAL_SOURCES=${MOUNT_LOCAL_SOURCES:="false"}
+export MOUNT_HOST_VOLUMES=${MOUNT_HOST_VOLUMES:="false"}
 
 # whethere verbose output should be produced
 export AIRFLOW_CI_VERBOSE=${VERBOSE}
@@ -52,55 +52,50 @@ export AIRFLOW_CI_VERBOSE=${VERBOSE}
 # opposite - whether diagnostict messages should be silenced
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-if [[ ${MOUNT_LOCAL_SOURCES} == "true" ]]; then
+if [[ ${MOUNT_HOST_VOLUMES} == "true" ]]; then
     DOCKER_COMPOSE_LOCAL=("-f" "${MY_DIR}/docker-compose-local.yml")
 else
     DOCKER_COMPOSE_LOCAL=()
 fi
 
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
-${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
-
-echo
-echo "Using docker image: ${AIRFLOW_CONTAINER_DOCKER_IMAGE} for docker compose runs"
-echo
-
 HOST_USER_ID="$(id -ur)"
 export HOST_USER_ID
 
 HOST_GROUP_ID="$(id -gr)"
 export HOST_GROUP_ID
 
-set +u
-if [[ "${ENV}" == "docker" ]]; then
-  docker-compose --log-level INFO \
-      -f "${MY_DIR}/docker-compose.yml" \
-      -f "${MY_DIR}/docker-compose-${BACKEND}.yml" \
-      "${DOCKER_COMPOSE_LOCAL[@]}" \
-        run airflow-testing /opt/airflow/scripts/ci/in_container/entrypoint_ci.sh;
-elif [[ "${ENV}" == "kubernetes" ]]; then
-  echo
-  echo "Running kubernetes tests in ${KUBERNETES_MODE}"
-  echo
-  docker-compose --log-level ERROR \
+if [[ ${START_KUBERNETES_CLUSTER} == "true" ]]; then
+    export KUBERNETES_MODE=${KUBERNETES_MODE:="git_mode"}
+    export KUBERNETES_VERSION=${KUBERNETES_VERSION:="v1.15.3"}
+
+    _build_and_save_kubernetes_image
+
+    set +u
+    # shellcheck disable=SC2016
+    docker-compose --log-level INFO \
       -f "${MY_DIR}/docker-compose.yml" \
       -f "${MY_DIR}/docker-compose-${BACKEND}.yml" \
+      -f "${MY_DIR}/docker-compose-kubernetes.yml" \
       "${DOCKER_COMPOSE_LOCAL[@]}" \
-         run airflow-testing /opt/airflow/scripts/ci/in_container/entrypoint_ci.sh;
-  echo
-  echo "Finished Running kubernetes tests in ${KUBERNETES_MODE}"
-  echo
-elif [[ "${ENV}" == "bare" ]]; then
-  docker-compose --log-level INFO \
+         run airflow-testing \
+           '/opt/airflow/scripts/ci/in_container/entrypoint_ci.sh "${@}"' \
+           /opt/airflow/scripts/ci/in_container/entrypoint_ci.sh "${@}"
+         # Note the command is there twice (!) because it is passed via bash -c
+         # and bash -c starts passing parameters from $0
+    set -u
+else
+    set +u
+    # shellcheck disable=SC2016
+    docker-compose --log-level INFO \
       -f "${MY_DIR}/docker-compose.yml" \
       -f "${MY_DIR}/docker-compose-${BACKEND}.yml" \
       "${DOCKER_COMPOSE_LOCAL[@]}" \
-        run --no-deps airflow-testing /opt/airflow/scripts/ci/in_container/entrypoint_ci.sh;
-else
-    echo >&2
-    echo >&2 "ERROR:  The ENV variable should be one of [docker, kubernetes, bare] and is '${ENV}'"
-    echo >&2
+         run airflow-testing \
+           '/opt/airflow/scripts/ci/in_container/entrypoint_ci.sh "${@}"' \
+           /opt/airflow/scripts/ci/in_container/entrypoint_ci.sh "${@}"
+         # Note the command is there twice (!) because it is passed via bash -c
+         # and bash -c starts passing parameters from $0
+    set -u
 fi
-set -u
 
 script_end
diff --git a/scripts/ci/ci_run_all_static_tests.sh b/scripts/ci/ci_run_all_static_tests.sh
index 5b59feb6acec1..881c3b4e48d3c 100755
--- a/scripts/ci/ci_run_all_static_tests.sh
+++ b/scripts/ci/ci_run_all_static_tests.sh
@@ -22,28 +22,26 @@ MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
-basic_sanity_checks
+# Set default python version
+export PYTHON_VERSION=${STATIC_CHECK_PYTHON_VERSION}
 
 script_start
 
-if [[ -f ${BUILD_CACHE_DIR}/.skip_tests ]]; then
-    echo
-    echo "Skip tests"
-    echo
-    script_end
-    exit
-fi
+initialize_environment
 
-rebuild_ci_image_if_needed
+prepare_build
+
+prepare_run
 
-IMAGES_TO_CHECK=("CI")
-export IMAGES_TO_CHECK
+export FORCE_ANSWER_TO_QUESTIONS="yes"
+rebuild_ci_image_if_needed
 
-pre-commit run --all-files --show-diff-on-failure
+SKIP=build pre-commit run --all-files --show-diff-on-failure
 
 script_end
diff --git a/scripts/ci/ci_isort.sh b/scripts/ci/ci_run_checklicence.sh
similarity index 67%
rename from scripts/ci/ci_isort.sh
rename to scripts/ci/ci_run_checklicence.sh
index 01e7b1163da31..827bc47ba4ac8 100755
--- a/scripts/ci/ci_isort.sh
+++ b/scripts/ci/ci_run_checklicence.sh
@@ -21,19 +21,30 @@ set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
-export ASSUME_QUIT_TO_ALL_QUESTIONS=${ASSUME_QUIT_TO_ALL_QUESTIONS:="true"}
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+export MOUNT_HOST_VOLUMES="true"
 
-force_python_3_5
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
+
+# Set default python version
+export PYTHON_VERSION=${STATIC_CHECK_PYTHON_VERSION}
 
 script_start
 
+initialize_environment
+
+prepare_build
+
+prepare_run
+
+export FORCE_ANSWER_TO_QUESTIONS="yes"
 rebuild_ci_image_if_needed
 
-run_flake8 "$@"
+export FORCE_ANSWER_TO_QUESTIONS="quit"
+pre-commit run check-apache-license --all-files --show-diff-on-failure
 
 script_end
diff --git a/scripts/ci/kubernetes/docker/Dockerfile b/scripts/ci/docker-compose-kubernetes.yml
similarity index 71%
rename from scripts/ci/kubernetes/docker/Dockerfile
rename to scripts/ci/docker-compose-kubernetes.yml
index 6ccb44358765f..0ac8fe4824fdd 100644
--- a/scripts/ci/kubernetes/docker/Dockerfile
+++ b/scripts/ci/docker-compose-kubernetes.yml
@@ -14,12 +14,14 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-ARG AIRFLOW_CI_IMAGE
-FROM ${AIRFLOW_CI_IMAGE}
-
-COPY airflow-test-env-init.sh /tmp/airflow-test-env-init.sh
-
-COPY bootstrap.sh /bootstrap.sh
-RUN chmod +x /bootstrap.sh
-
-ENTRYPOINT ["/bootstrap.sh"]
+---
+version: "2.2"
+services:
+  airflow-testing:
+    environment:
+      - KUBERNETES_MODE
+      - KUBERNETES_VERSION
+      - RECREATE_KUBERNETES_CLUSTER
+    volumes:
+      - ${AIRFLOW_CI_SAVED_IMAGE_DIR}:/opt/airflow/.docker_image_dir:ro
+      - ${AIRFLOW_CI_SAVED_IMAGE_ID_FILE}:/opt/airflow/.docker_image.sha256:ro
diff --git a/scripts/ci/docker-compose-local.yml b/scripts/ci/docker-compose-local.yml
index 1cc0c2f3ddf80..cedebbe388ac1 100644
--- a/scripts/ci/docker-compose-local.yml
+++ b/scripts/ci/docker-compose-local.yml
@@ -31,6 +31,7 @@ services:
     # tests (such as logs)
     volumes:
       - ../../airflow:/opt/airflow/airflow:cached
+      - ../../common:/opt/airflow/common:cached
       - ../../setup.cfg:/opt/airflow/setup.cfg:cached
       - ../../setup.py:/opt/airflow/setup.py:cached
       - ../../dags:/opt/airflow/dags:cached
@@ -46,12 +47,14 @@ services:
       - ../../CHANGELOG.txt:/opt/airflow/CHANGELOG:cached
       - ../../.github:/opt/airflow/.github:cached
       - ../../.bash_completion.d:/root/.bash_completion.d:cached
+      - ../../.kube:/root/.kube:cached
       - ../../.bash_history:/root/.bash_history:cached
       - ../../.bash_aliases:/root/.bash_aliases:cached
       - ../../.inputrc:/root/.inputrc:cached
       - ../../.flake8:/opt/airflow/.flake8:cached
       - ../../pylintrc:/opt/airflow/pylintrc:cached
       - ../../.rat-excludes:/opt/airflow/.rat-excludes:cached
+      - ../../.dockerignore:/opt/airflow/.dockerignore:cached
       - ../../run-tests:/root/run-tests:cached
       - ../../run-tests-complete:/root/run-tests-complete:cached
       - ../../logs:/root/airflow/logs:cached
diff --git a/scripts/ci/docker-compose.yml b/scripts/ci/docker-compose.yml
index 2eec9001b3851..2bdf2eacfcb8a 100644
--- a/scripts/ci/docker-compose.yml
+++ b/scripts/ci/docker-compose.yml
@@ -69,7 +69,7 @@ services:
       - DOCKER_TLS_VERIFY=
 
   airflow-testing:
-    image: ${AIRFLOW_CONTAINER_DOCKER_IMAGE}
+    image: ${AIRFLOW_CI_IMAGE}
     init: true
     entrypoint: ["/bin/bash", "-c"]
     environment:
@@ -84,7 +84,6 @@ services:
       - KRB5_KTNAME=/etc/airflow.keytab
       - CELERY_BROKER_URLS=amqp://guest:guest@rabbitmq:5672,redis://redis:6379/0
       - DOCKER_HOST=tcp://docker:2375
-      - ENV
       - BACKEND
       - CI
       - SOURCE_BRANCH
@@ -101,9 +100,9 @@ services:
       - RUN_TESTS
       - AIRFLOW_CI_VERBOSE
       - AIRFLOW_CI_SILENT
-      - AIRFLOW_CONTAINER_DOCKER_IMAGE
-      - KUBERNETES_VERSION
-      - KUBERNETES_MODE
+      - AIRFLOW_CI_IMAGE
+      - START_KUBERNETES_CLUSTER
+      - DEPS
     depends_on:
       - postgres
       - mysql
diff --git a/scripts/ci/in_container/_in_container_utils.sh b/scripts/ci/in_container/_in_container_utils.sh
old mode 100644
new mode 100755
index 62077c3909b1f..9382923e17cb5
--- a/scripts/ci/in_container/_in_container_utils.sh
+++ b/scripts/ci/in_container/_in_container_utils.sh
@@ -57,13 +57,13 @@ function in_container_cleanup_pyc() {
     print_in_container_info "Cleaning up .pyc files"
     print_in_container_info
     set +o pipefail
-    NUM_FILES=$(sudo find . \
+    NUM_FILES=$(gosu root find . \
         -path "./airflow/www/node_modules" -prune -o \
         -path "./airflow/www_rbac/node_modules" -prune -o \
         -path "./.eggs" -prune -o \
         -path "./docs/_build" -prune -o \
         -path "./build" -prune -o \
-        -name "*.pyc" | grep ".pyc$" | sudo xargs rm -vf | wc -l)
+        -name "*.pyc" | grep ".pyc$" | gosu root xargs rm -vf | wc -l)
     print_in_container_info "Number of deleted .pyc files: ${NUM_FILES}"
     set -o pipefail
     print_in_container_info
@@ -84,7 +84,7 @@ function in_container_cleanup_pycache() {
         -path "./.eggs" -prune -o \
         -path "./docs/_build" -prune -o \
         -path "./build" -prune -o \
-        -name "__pycache__" | grep "__pycache__" | sudo xargs rm -rvf | wc -l)
+        -name "__pycache__" | grep "__pycache__" | gosu root xargs rm -rvf | wc -l)
     print_in_container_info "Number of deleted __pycache__ dirs (and files): ${NUM_FILES}"
     set -o pipefail
     print_in_container_info
@@ -96,15 +96,17 @@ function in_container_cleanup_pycache() {
 # The host user.
 #
 function in_container_fix_ownership() {
-    print_in_container_info
-    print_in_container_info "Changing ownership of root-owned files to ${HOST_USER_ID}.${HOST_GROUP_ID}"
-    print_in_container_info
-    set +o pipefail
-    sudo find . -user root | sudo xargs chown -v "${HOST_USER_ID}.${HOST_GROUP_ID}" | wc -l | \
-        xargs -n 1 echo "Number of files with changed ownership:"
-    set -o pipefail
-    print_in_container_info
-    print_in_container_info
+    if [[ -n ${HOST_USER_ID:=} && -n ${HOST_GROUP_ID:=} ]]; then
+        print_in_container_info
+        print_in_container_info "Changing ownership of root-owned files to ${HOST_USER_ID}.${HOST_GROUP_ID}"
+        print_in_container_info
+        set +o pipefail
+        gosu root find . -user root | gosu root xargs chown -v "${HOST_USER_ID}.${HOST_GROUP_ID}" | wc -l | \
+            xargs -n 1 echo "Number of files with changed ownership:"
+        set -o pipefail
+        print_in_container_info
+        print_in_container_info
+    fi
 }
 
 function in_container_go_to_airflow_sources() {
diff --git a/scripts/ci/in_container/entrypoint_ci.sh b/scripts/ci/in_container/entrypoint_ci.sh
index 90ebd8bd4bc92..4bb46a0350ce4 100755
--- a/scripts/ci/in_container/entrypoint_ci.sh
+++ b/scripts/ci/in_container/entrypoint_ci.sh
@@ -35,11 +35,11 @@ in_container_script_start
 
 AIRFLOW_SOURCES=$(cd "${MY_DIR}/../../.." || exit 1; pwd)
 
-PYTHON_VERSION=${PYTHON_VERSION:=3.6}
-ENV=${ENV:=docker}
 BACKEND=${BACKEND:=sqlite}
-KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
 KUBERNETES_MODE=${KUBERNETES_MODE:=""}
+KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
+RECREATE_KUBERNETES_CLUSTER=${RECREATE_KUBERNETES_CLUSTER:="true"}
+START_KUBERNETES_CLUSTER=${START_KUBERNETES_CLUSTER:="false"}
 
 export AIRFLOW_HOME=${AIRFLOW_HOME:=${HOME}}
 
@@ -136,13 +136,13 @@ sudo rm -rf "${AIRFLOW_SOURCES}"/tmp/*
 mkdir -p "${AIRFLOW_SOURCES}"/logs/
 mkdir -p "${AIRFLOW_SOURCES}"/tmp/
 
-if [[ "${ENV}" == "docker" ]]; then
+if [[ "${START_KUBERNETES_CLUSTER}" == "false" ]]; then
     # Start MiniCluster
     java -cp "/opt/minicluster-1.1-SNAPSHOT/*" com.ing.minicluster.MiniCluster \
         >"${AIRFLOW_HOME}/logs/minicluster.log" 2>&1 &
 
     # Set up ssh keys
-    echo 'yes' | ssh-keygen -t rsa -C your_email@youremail.com -P '' -f ~/.ssh/id_rsa \
+    echo 'yes' | ssh-keygen -t rsa -C your_email@youremail.com -m PEM -P '' -f ~/.ssh/id_rsa \
         >"${AIRFLOW_HOME}/logs/ssh-keygen.log" 2>&1
 
     cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
@@ -152,52 +152,54 @@ if [[ "${ENV}" == "docker" ]]; then
     # SSH Service
     sudo service ssh restart >/dev/null 2>&1
 
-    # Setting up kerberos
-
-    FQDN=$(hostname)
-    ADMIN="admin"
-    PASS="airflow"
-    KRB5_KTNAME=/etc/airflow.keytab
-
-    if [[ ${AIRFLOW_CI_VERBOSE} == "true" ]]; then
-        echo
-        echo "Hosts:"
-        echo
-        cat /etc/hosts
-        echo
-        echo "Hostname: ${FQDN}"
-        echo
-    fi
-
-    sudo cp "${MY_DIR}/krb5/krb5.conf" /etc/krb5.conf
-
-    set +e
-    echo -e "${PASS}\n${PASS}" | \
-        sudo kadmin -p "${ADMIN}/admin" -w "${PASS}" -q "addprinc -randkey airflow/${FQDN}" 2>&1 \
-          | sudo tee "${AIRFLOW_HOME}/logs/kadmin_1.log" >/dev/null
-    RES_1=$?
-
-    sudo kadmin -p "${ADMIN}/admin" -w "${PASS}" -q "ktadd -k ${KRB5_KTNAME} airflow" 2>&1 \
-          | sudo tee "${AIRFLOW_HOME}/logs/kadmin_2.log" >/dev/null
-    RES_2=$?
-
-    sudo kadmin -p "${ADMIN}/admin" -w "${PASS}" -q "ktadd -k ${KRB5_KTNAME} airflow/${FQDN}" 2>&1 \
-          | sudo tee "${AIRFLOW_HOME}/logs/kadmin_3.log" >/dev/null
-    RES_3=$?
-    set -e
-
-    if [[ ${RES_1} != 0 || ${RES_2} != 0 || ${RES_3} != 0 ]]; then
-        echo
-        echo "ERROR:  There was a problem communicating with kerberos"
-        echo "Errors produced by kadmin commands are in : ${AIRFLOW_HOME}/logs/kadmin*.log"
-        echo
-        echo "Action! Please restart the environment!"
-        echo "Run './scripts/ci/local_ci_stop_environment.sh' and re-enter the environment"
-        echo
-        exit 1
+    if [[ ${DEPS:="true"} == "true" ]]; then
+        # Setting up kerberos
+
+        FQDN=$(hostname)
+        ADMIN="admin"
+        PASS="airflow"
+        KRB5_KTNAME=/etc/airflow.keytab
+
+        if [[ ${AIRFLOW_CI_VERBOSE} == "true" ]]; then
+            echo
+            echo "Hosts:"
+            echo
+            cat /etc/hosts
+            echo
+            echo "Hostname: ${FQDN}"
+            echo
+        fi
+
+        sudo cp "${MY_DIR}/krb5/krb5.conf" /etc/krb5.conf
+
+        set +e
+        echo -e "${PASS}\n${PASS}" | \
+            sudo kadmin -p "${ADMIN}/admin" -w "${PASS}" -q "addprinc -randkey airflow/${FQDN}" 2>&1 \
+              | sudo tee "${AIRFLOW_HOME}/logs/kadmin_1.log" >/dev/null
+        RES_1=$?
+
+        sudo kadmin -p "${ADMIN}/admin" -w "${PASS}" -q "ktadd -k ${KRB5_KTNAME} airflow" 2>&1 \
+              | sudo tee "${AIRFLOW_HOME}/logs/kadmin_2.log" >/dev/null
+        RES_2=$?
+
+        sudo kadmin -p "${ADMIN}/admin" -w "${PASS}" -q "ktadd -k ${KRB5_KTNAME} airflow/${FQDN}" 2>&1 \
+              | sudo tee "${AIRFLOW_HOME}/logs/kadmin_3.log" >/dev/null
+        RES_3=$?
+        set -e
+
+        if [[ ${RES_1} != 0 || ${RES_2} != 0 || ${RES_3} != 0 ]]; then
+            echo
+            echo "ERROR:  There was a problem communicating with kerberos"
+            echo "Errors produced by kadmin commands are in : ${AIRFLOW_HOME}/logs/kadmin*.log"
+            echo
+            echo "Action! Please restart the environment!"
+            echo "Run './scripts/ci/local_ci_stop_environment.sh' and re-enter the environment"
+            echo
+            exit 1
+        fi
+
+        sudo chmod 0644 "${KRB5_KTNAME}"
     fi
-
-    sudo chmod 0644 "${KRB5_KTNAME}"
 fi
 
 # Exporting XUNIT_FILE so that we can see summary of failed tests
@@ -207,6 +209,11 @@ mkdir -pv "${AIRFLOW_HOME}/logs/"
 
 cp -f "${MY_DIR}/airflow_ci.cfg" "${AIRFLOW_HOME}/unittests.cfg"
 
+if [[ "${START_KUBERNETES_CLUSTER}" == "true" ]]; then
+    "${MY_DIR}/kubernetes/setup_kubernetes.sh"
+    "${MY_DIR}/kubernetes/app/deploy_app.sh"
+fi
+
 set +u
 # If we do not want to run tests, we simply drop into bash
 if [[ "${RUN_TESTS}" == "false" ]]; then
@@ -217,10 +224,9 @@ if [[ "${RUN_TESTS}" == "false" ]]; then
     else
         exec /bin/bash -c "$(printf "%q " "${ARGS[@]}")"
     fi
-fi
-
-if [[ ${#ARGS} == 0 ]]; then
-    ARGS=("--with-coverage"
+else
+    EXTRA_ARGS=(
+          "--with-coverage"
           "--cover-erase"
           "--cover-html"
           "--cover-package=airflow"
@@ -231,33 +237,23 @@ if [[ ${#ARGS} == 0 ]]; then
           "--xunit-file=${XUNIT_FILE}"
           "--with-timer"
           "-v"
-          "--logging-level=INFO")
-    echo
-    echo "Running ALL Tests"
-    echo
-else
-    echo
-    echo "Running tests with ${ARGS[*]}"
-    echo
-fi
-set -u
-
-KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
-
-if [[ -z "${KUBERNETES_VERSION}" ]]; then
-    echo
-    echo "Running CI tests with ${ARGS[*]}"
-    echo
-    "${MY_DIR}/run_ci_tests.sh" "${ARGS[@]}"
-else
-    echo "Set up Kubernetes cluster for tests"
-    "${MY_DIR}/../kubernetes/setup_kubernetes.sh"
-    "${MY_DIR}/../kubernetes/app/deploy_app.sh" -d "${KUBERNETES_MODE}"
+          "--logging-level=INFO"
+    )
 
+    if [[ ${#ARGS} == 0 ]]; then
+        echo
+        echo "Running ALL Tests"
+        echo
+    else
+        echo
+        echo "Running tests: ${ARGS[*]}"
+        echo
+    fi
     echo
-    echo "Running CI tests with ${ARGS[*]}"
+    echo "Running CI tests with ${EXTRA_ARGS[*]} ${ARGS[*]}"
     echo
-    "${MY_DIR}/run_ci_tests.sh" tests.integration.kubernetes "${ARGS[@]}"
+    "${MY_DIR}/run_ci_tests.sh" "${EXTRA_ARGS[@]}" "${ARGS[@]}"
 fi
+set -u
 
 in_container_script_end
diff --git a/scripts/ci/in_container/kubernetes/app/deploy_app.sh b/scripts/ci/in_container/kubernetes/app/deploy_app.sh
new file mode 100755
index 0000000000000..65cdb83d5d712
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/app/deploy_app.sh
@@ -0,0 +1,183 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -eu
+set -x
+MY_DIR=$(cd "$(dirname "$0")" && pwd)
+
+AIRFLOW_SOURCES=$(
+    cd "${MY_DIR}/../../../../../" || exit 1
+    pwd
+)
+export AIRFLOW_SOURCES
+
+# shellcheck source=scripts/ci/in_container/_in_container_utils.sh
+. "${MY_DIR}/../../_in_container_utils.sh"
+
+assert_in_container
+
+in_container_script_start
+
+export TEMPLATE_DIRNAME="${MY_DIR}/templates"
+export BUILD_DIRNAME="${MY_DIR}/build"
+
+# shellcheck source=scripts/ci/utils/_common_variables.sh
+. "${AIRFLOW_SOURCES}/scripts/ci/utils/_common_variables.sh"
+
+# Source branch will be set in DockerHub
+SOURCE_BRANCH=${SOURCE_BRANCH:=${DEFAULT_BRANCH}}
+# if BRANCH_NAME is not set it will be set to either SOURCE_BRANCH (if overridden)
+# or default branch for the sources
+BRANCH_NAME=${BRANCH_NAME:=${SOURCE_BRANCH}}
+
+if [[ ! -d "${BUILD_DIRNAME}" ]]; then
+    mkdir -p "${BUILD_DIRNAME}"
+fi
+
+rm -f "${BUILD_DIRNAME}"/*
+
+if [[ "${KUBERNETES_MODE}" == "persistent_mode" ]]; then
+    INIT_DAGS_VOLUME_NAME=airflow-dags
+    POD_AIRFLOW_DAGS_VOLUME_NAME=airflow-dags
+    CONFIGMAP_DAGS_FOLDER=/root/airflow/dags
+    CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT=
+    CONFIGMAP_DAGS_VOLUME_CLAIM=airflow-dags
+else
+    INIT_DAGS_VOLUME_NAME=airflow-dags-fake
+    POD_AIRFLOW_DAGS_VOLUME_NAME=airflow-dags-git
+    CONFIGMAP_DAGS_FOLDER=/root/airflow/dags/repo/airflow/contrib/example_dags
+    CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT=/root/airflow/dags
+    CONFIGMAP_DAGS_VOLUME_CLAIM=
+fi
+
+CONFIGMAP_GIT_REPO=${TRAVIS_REPO_SLUG:-apache/airflow}
+CONFIGMAP_BRANCH=${DEFAULT_BRANCH:=master}
+
+if [[ "${KUBERNETES_MODE}" == "persistent_mode" ]]; then
+    sed -e "s/{{INIT_GIT_SYNC}}//g" \
+        "${TEMPLATE_DIRNAME}/airflow.template.yaml" >"${BUILD_DIRNAME}/airflow.yaml"
+else
+    sed -e "/{{INIT_GIT_SYNC}}/{r ${TEMPLATE_DIRNAME}/init_git_sync.template.yaml" -e 'd}' \
+        "${TEMPLATE_DIRNAME}/airflow.template.yaml" >"${BUILD_DIRNAME}/airflow.yaml"
+fi
+sed -i "s|{{AIRFLOW_CI_KUBERNETES_IMAGE}}|${AIRFLOW_CI_KUBERNETES_IMAGE}|g" "${BUILD_DIRNAME}/airflow.yaml"
+
+sed -i "s|{{CONFIGMAP_GIT_REPO}}|${CONFIGMAP_GIT_REPO}|g" "${BUILD_DIRNAME}/airflow.yaml"
+sed -i "s|{{CONFIGMAP_BRANCH}}|${CONFIGMAP_BRANCH}|g" "${BUILD_DIRNAME}/airflow.yaml"
+sed -i "s|{{INIT_DAGS_VOLUME_NAME}}|${INIT_DAGS_VOLUME_NAME}|g" "${BUILD_DIRNAME}/airflow.yaml"
+sed -i "s|{{POD_AIRFLOW_DAGS_VOLUME_NAME}}|${POD_AIRFLOW_DAGS_VOLUME_NAME}|g" \
+    "${BUILD_DIRNAME}/airflow.yaml"
+
+sed "s|{{CONFIGMAP_DAGS_FOLDER}}|${CONFIGMAP_DAGS_FOLDER}|g" \
+    "${TEMPLATE_DIRNAME}/configmaps.template.yaml" >"${BUILD_DIRNAME}/configmaps.yaml"
+sed -i "s|{{CONFIGMAP_GIT_REPO}}|${CONFIGMAP_GIT_REPO}|g" "${BUILD_DIRNAME}/configmaps.yaml"
+sed -i "s|{{CONFIGMAP_BRANCH}}|${CONFIGMAP_BRANCH}|g" "${BUILD_DIRNAME}/configmaps.yaml"
+sed -i "s|{{CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT}}|${CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT}|g" \
+    "${BUILD_DIRNAME}/configmaps.yaml"
+sed -i "s|{{CONFIGMAP_DAGS_VOLUME_CLAIM}}|${CONFIGMAP_DAGS_VOLUME_CLAIM}|g" \
+    "${BUILD_DIRNAME}/configmaps.yaml"
+
+cat "${BUILD_DIRNAME}/airflow.yaml"
+cat "${BUILD_DIRNAME}/configmaps.yaml"
+
+kubectl delete -f "${MY_DIR}/postgres.yaml" || true
+kubectl delete -f "${BUILD_DIRNAME}/airflow.yaml" || true
+kubectl delete -f "${MY_DIR}/secrets.yaml" || true
+
+set -e
+
+kubectl apply -f "${MY_DIR}/secrets.yaml"
+kubectl apply -f "${BUILD_DIRNAME}/configmaps.yaml"
+kubectl apply -f "${MY_DIR}/postgres.yaml"
+kubectl apply -f "${MY_DIR}/volumes.yaml"
+kubectl apply -f "${BUILD_DIRNAME}/airflow.yaml"
+
+dump_logs() {
+    POD=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}' | grep airflow | head -1)
+    echo "------- pod description -------"
+    kubectl describe pod "${POD}"
+    echo "------- webserver init container logs - init -------"
+    kubectl logs "${POD}" -c init || true
+    if [[ "${KUBERNETES_MODE}" != "persistent_mode" ]]; then
+        echo "------- webserver init container logs - git-sync-clone -------"
+        kubectl logs "${POD}" -c git-sync-clone || true
+    fi
+    echo "------- webserver logs -------"
+    kubectl logs "${POD}" -c webserver || true
+    echo "------- scheduler logs -------"
+    kubectl logs "${POD}" -c scheduler || true
+    echo "--------------"
+}
+
+set +x
+# wait for up to 10 minutes for everything to be deployed
+PODS_ARE_READY="0"
+for i in {1..150}; do
+    echo "------- Running kubectl get pods: $i -------"
+    PODS=$(kubectl get pods | awk 'NR>1 {print $0}')
+    echo "$PODS"
+    NUM_AIRFLOW_READY=$(echo "${PODS}" | grep airflow | awk '{print $2}' | grep -cE '([0-9])\/(\1)' | xargs)
+    NUM_POSTGRES_READY=$(echo "${PODS}" | grep postgres | awk '{print $2}' | grep -cE '([0-9])\/(\1)' | xargs)
+    if [[ "${NUM_AIRFLOW_READY}" == "1" && "${NUM_POSTGRES_READY}" == "1" ]]; then
+        PODS_ARE_READY="1"
+        break
+    fi
+    sleep 4
+done
+POD=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}' | grep airflow | head -1)
+
+if [[ "${PODS_ARE_READY}" == "1" ]]; then
+    echo "PODS are ready."
+else
+    echo >&2 "PODS are not ready after waiting for a long time. Exiting..."
+    dump_logs
+    exit 1
+fi
+
+# Wait until Airflow webserver is up
+KUBERNETES_HOST=docker
+AIRFLOW_WEBSERVER_IS_READY="0"
+CONSECUTIVE_SUCCESS_CALLS=0
+for i in {1..30}; do
+    echo "------- Wait until webserver is up: $i -------"
+    PODS=$(kubectl get pods | awk 'NR>1 {print $0}')
+    echo "$PODS"
+    HTTP_CODE=$(curl -LI "http://${KUBERNETES_HOST}:30809/health" -o /dev/null -w '%{http_code}\n' -sS) || true
+    if [[ "${HTTP_CODE}" == 200 ]]; then
+        ((CONSECUTIVE_SUCCESS_CALLS += 1))
+    else
+        CONSECUTIVE_SUCCESS_CALLS="0"
+    fi
+    if [[ "${CONSECUTIVE_SUCCESS_CALLS}" == 3 ]]; then
+        AIRFLOW_WEBSERVER_IS_READY="1"
+        break
+    fi
+    sleep 10
+done
+
+if [[ "${AIRFLOW_WEBSERVER_IS_READY}" == "1" ]]; then
+    echo "Airflow webserver is ready."
+else
+    echo >&2 "Airflow webserver is not ready after waiting for a long time. Exiting..."
+    dump_logs
+    exit 1
+fi
+
+dump_logs
+
+in_container_script_end
diff --git a/scripts/ci/kubernetes/app/postgres.yaml b/scripts/ci/in_container/kubernetes/app/postgres.yaml
similarity index 100%
rename from scripts/ci/kubernetes/app/postgres.yaml
rename to scripts/ci/in_container/kubernetes/app/postgres.yaml
diff --git a/scripts/ci/kubernetes/app/secrets.yaml b/scripts/ci/in_container/kubernetes/app/secrets.yaml
similarity index 100%
rename from scripts/ci/kubernetes/app/secrets.yaml
rename to scripts/ci/in_container/kubernetes/app/secrets.yaml
diff --git a/scripts/ci/kubernetes/app/templates/airflow.template.yaml b/scripts/ci/in_container/kubernetes/app/templates/airflow.template.yaml
similarity index 96%
rename from scripts/ci/kubernetes/app/templates/airflow.template.yaml
rename to scripts/ci/in_container/kubernetes/app/templates/airflow.template.yaml
index bc0e67bd228e1..8ed1b90f84802 100644
--- a/scripts/ci/kubernetes/app/templates/airflow.template.yaml
+++ b/scripts/ci/in_container/kubernetes/app/templates/airflow.template.yaml
@@ -43,7 +43,7 @@ spec:
     spec:
       initContainers:
       - name: "init"
-        image: {{AIRFLOW_IMAGE}}:{{AIRFLOW_TAG}}
+        image: {{AIRFLOW_CI_KUBERNETES_IMAGE}}
         imagePullPolicy: IfNotPresent
         volumeMounts:
         - name: airflow-configmap
@@ -67,7 +67,7 @@ spec:
 {{INIT_GIT_SYNC}}
       containers:
       - name: webserver
-        image: {{AIRFLOW_IMAGE}}:{{AIRFLOW_TAG}}
+        image: {{AIRFLOW_CI_KUBERNETES_IMAGE}}
         imagePullPolicy: IfNotPresent
         ports:
         - name: webserver
@@ -92,7 +92,7 @@ spec:
         - name: airflow-logs
           mountPath: /root/airflow/logs
       - name: scheduler
-        image: {{AIRFLOW_IMAGE}}:{{AIRFLOW_TAG}}
+        image: {{AIRFLOW_CI_KUBERNETES_IMAGE}}
         imagePullPolicy: IfNotPresent
         args: ["scheduler"]
         env:
diff --git a/scripts/ci/kubernetes/app/templates/configmaps.template.yaml b/scripts/ci/in_container/kubernetes/app/templates/configmaps.template.yaml
similarity index 100%
rename from scripts/ci/kubernetes/app/templates/configmaps.template.yaml
rename to scripts/ci/in_container/kubernetes/app/templates/configmaps.template.yaml
diff --git a/scripts/ci/kubernetes/app/templates/init_git_sync.template.yaml b/scripts/ci/in_container/kubernetes/app/templates/init_git_sync.template.yaml
similarity index 100%
rename from scripts/ci/kubernetes/app/templates/init_git_sync.template.yaml
rename to scripts/ci/in_container/kubernetes/app/templates/init_git_sync.template.yaml
diff --git a/scripts/ci/kubernetes/app/volumes.yaml b/scripts/ci/in_container/kubernetes/app/volumes.yaml
similarity index 100%
rename from scripts/ci/kubernetes/app/volumes.yaml
rename to scripts/ci/in_container/kubernetes/app/volumes.yaml
diff --git a/scripts/ci/kubernetes/docker/airflow-test-env-init.sh b/scripts/ci/in_container/kubernetes/docker/airflow-test-env-init.sh
similarity index 100%
rename from scripts/ci/kubernetes/docker/airflow-test-env-init.sh
rename to scripts/ci/in_container/kubernetes/docker/airflow-test-env-init.sh
diff --git a/scripts/ci/kubernetes/docker/bootstrap.sh b/scripts/ci/in_container/kubernetes/docker/bootstrap.sh
similarity index 100%
rename from scripts/ci/kubernetes/docker/bootstrap.sh
rename to scripts/ci/in_container/kubernetes/docker/bootstrap.sh
diff --git a/scripts/ci/in_container/kubernetes/docker/load_airflow_image.sh b/scripts/ci/in_container/kubernetes/docker/load_airflow_image.sh
new file mode 100755
index 0000000000000..1ca9e72529094
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/docker/load_airflow_image.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+set -euo pipefail
+MY_DIR=$(cd "$(dirname "$0")" && pwd)
+
+AIRFLOW_SOURCES=$(cd "${MY_DIR}/../../../../../" || exit 1 ; pwd)
+export AIRFLOW_SOURCES
+
+# shellcheck source=scripts/ci/in_container/_in_container_utils.sh
+. "${MY_DIR}/../../_in_container_utils.sh"
+
+assert_in_container
+
+in_container_script_start
+
+export AIRFLOW_CI_SAVED_IMAGE_DIR=/opt/airflow/.docker_image_dir
+export AIRFLOW_CI_SAVED_IMAGE_ID_FILE=/opt/airflow/.docker_image.sha256
+export CLUSTER_NAME="airflow-${KUBERNETES_VERSION}"
+
+DOCKER_IMAGE_ID=$(docker inspect --format='{{index .Id}}' "${AIRFLOW_CI_IMAGE}" 2>/dev/null || true)
+SAVED_IMAGE_ID=$(cat "${AIRFLOW_CI_SAVED_IMAGE_ID_FILE}")
+if [[ "${SAVED_IMAGE_ID}" == "${DOCKER_IMAGE_ID}" ]]; then
+    echo
+    echo "Skip Loading the ${AIRFLOW_CI_IMAGE} as it has not changed."
+    echo
+else
+    echo
+    echo "Loading the ${AIRFLOW_CI_IMAGE} from ${AIRFLOW_CI_SAVED_IMAGE_DIR} to docker"
+    echo
+    tar -cC "${AIRFLOW_CI_SAVED_IMAGE_DIR}" . | docker image load
+    echo
+    echo "Loading the ${AIRFLOW_CI_IMAGE} to cluster ${CLUSTER_NAME} from docker"
+    echo
+    kind load docker-image --name "${CLUSTER_NAME}" "${AIRFLOW_CI_IMAGE}"
+    echo
+    echo "Loaded the ${AIRFLOW_CI_SAVED_IMAGE_DIR} dir to ${AIRFLOW_CI_IMAGE} in docker and cluster ${CLUSTER_NAME}"
+    echo
+fi
+
+in_container_script_end
diff --git a/scripts/ci/in_container/kubernetes/docker/rebuild_airflow_image.sh b/scripts/ci/in_container/kubernetes/docker/rebuild_airflow_image.sh
new file mode 100755
index 0000000000000..f25094b05e329
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/docker/rebuild_airflow_image.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+set -euo pipefail
+MY_DIR=$(cd "$(dirname "$0")" && pwd)
+
+AIRFLOW_SOURCES=$(cd "${MY_DIR}/../../../../../" || exit 1 ; pwd)
+export AIRFLOW_SOURCES
+
+# shellcheck source=scripts/ci/in_container/_in_container_utils.sh
+. "${MY_DIR}/../../_in_container_utils.sh"
+
+assert_in_container
+
+in_container_script_start
+
+export CLUSTER_NAME="airflow-${KUBERNETES_VERSION}"
+
+cd "${AIRFLOW_SOURCES}"
+
+echo
+echo "(Re)building kubernetes image from ${AIRFLOW_CI_IMAGE} with latest sources"
+echo
+docker build \
+    --build-arg AIRFLOW_CI_IMAGE="${AIRFLOW_CI_IMAGE}" \
+    --cache-from "${AIRFLOW_CI_IMAGE}" \
+    --tag="${AIRFLOW_CI_IMAGE}" \
+    -f- . <<EOF
+ARG AIRFLOW_CI_IMAGE
+FROM ${AIRFLOW_CI_IMAGE}
+
+COPY scripts/ci/in_container/kubernetes/docker/airflow-test-env-init.sh /tmp/airflow-test-env-init.sh
+
+COPY scripts/ci/in_container/kubernetes/docker/bootstrap.sh /bootstrap.sh
+
+RUN chmod +x /bootstrap.sh
+
+COPY airflow/ ${AIRFLOW_SOURCES}/airflow/
+
+ENTRYPOINT ["/bootstrap.sh"]
+EOF
+
+echo
+echo "Loading the ${AIRFLOW_CI_IMAGE} to cluster ${CLUSTER_NAME} from docker"
+echo
+kind load docker-image --name "${CLUSTER_NAME}" "${AIRFLOW_CI_IMAGE}"
+echo
+echo "Loaded the ${AIRFLOW_CI_IMAGE} in cluster ${CLUSTER_NAME}"
+echo
+
+in_container_script_end
diff --git a/scripts/ci/kubernetes/kind-cluster-conf.yaml b/scripts/ci/in_container/kubernetes/kind-cluster-conf.yaml
similarity index 100%
rename from scripts/ci/kubernetes/kind-cluster-conf.yaml
rename to scripts/ci/in_container/kubernetes/kind-cluster-conf.yaml
diff --git a/scripts/ci/in_container/kubernetes/setup_kubernetes.sh b/scripts/ci/in_container/kubernetes/setup_kubernetes.sh
new file mode 100755
index 0000000000000..62654c97c3943
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/setup_kubernetes.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+MY_DIR=$(cd "$(dirname "$0")" && pwd)
+
+AIRFLOW_SOURCES=$(cd "${MY_DIR}/../../../../" || exit 1 ; pwd)
+export AIRFLOW_SOURCES
+
+export RECREATE_KUBERNETES_CLUSTER=${RECREATE_KUBERNETES_CLUSTER:="true"}
+
+# shellcheck source=scripts/ci/in_container/_in_container_utils.sh
+. "${MY_DIR}/../_in_container_utils.sh"
+
+assert_in_container
+
+in_container_script_start
+
+# just in case it's not set
+export KUBERNETES_VERSION=${KUBERNETES_VERSION:="v1.15.3"}
+
+echo
+echo "Kubernetes version = ${KUBERNETES_VERSION}"
+echo
+
+export CLUSTER_NAME="airflow-${KUBERNETES_VERSION}"
+
+function create_cluster {
+    kind create cluster --name "${CLUSTER_NAME}" \
+       --config "${MY_DIR}/kind-cluster-conf.yaml" --image "kindest/node:${KUBERNETES_VERSION}"
+    echo
+    echo "Created cluster ${CLUSTER_NAME}"
+    echo
+}
+
+function delete_cluster {
+    kind delete cluster --name "${CLUSTER_NAME}"
+    echo
+    echo "Deleted cluster ${CLUSTER_NAME}"
+    echo
+}
+
+ALL_CLUSTERS=$(kind get clusters || true)
+if [[ ${ALL_CLUSTERS} == *"${CLUSTER_NAME}"* ]]; then
+    echo
+    echo "Cluster ${CLUSTER_NAME} is already created"
+    echo
+    if [[ ${RECREATE_KUBERNETES_CLUSTER} == "true" ]]; then
+        echo
+        echo "Recreating cluster"
+        echo
+        delete_cluster
+        create_cluster
+    else
+        echo
+        echo "Reusing previously created cluster"
+        echo
+    fi
+else
+    echo
+    echo "Creating cluster"
+    echo
+    create_cluster
+fi
+
+cp "$(kind get kubeconfig-path --name "${CLUSTER_NAME}")" ~/.kube/config
+kubectl config set "clusters.${CLUSTER_NAME}.server" https://docker:19090
+kubectl cluster-info
+
+kubectl get nodes
+echo
+echo "Showing storageClass"
+echo
+kubectl get storageclass
+echo
+echo "Showing kube-system pods"
+echo
+kubectl get -n kube-system pods
+echo
+"${MY_DIR}/docker/load_airflow_image.sh"
+echo
+echo
+"${MY_DIR}/docker/rebuild_airflow_image.sh"
+echo
+echo
+echo "Airflow environment on kubernetes is good to go!"
+echo
+
+in_container_script_end
diff --git a/scripts/ci/in_container/run_check_licence.sh b/scripts/ci/in_container/run_check_licence.sh
index d486071efb89a..a16f2eec3db38 100755
--- a/scripts/ci/in_container/run_check_licence.sh
+++ b/scripts/ci/in_container/run_check_licence.sh
@@ -34,7 +34,10 @@ echo
 
 # This is the target of a symlink in airflow/www/static/docs -
 # and rat exclude doesn't cope with the symlink target doesn't exist
-sudo mkdir -p docs/_build/html/
+mkdir -p docs/_build/html/
+
+# This is wherre rat results are placed
+mkdir -p "${AIRFLOW_SOURCES}/logs/"
 
 echo "Running license checks. This can take a while."
 
diff --git a/scripts/ci/in_container/run_ci_tests.sh b/scripts/ci/in_container/run_ci_tests.sh
index e0dc3f14489d2..c2d3054b2f937 100755
--- a/scripts/ci/in_container/run_ci_tests.sh
+++ b/scripts/ci/in_container/run_ci_tests.sh
@@ -32,9 +32,10 @@ in_container_script_start
 # any argument received is overriding the default nose execution arguments:
 NOSE_ARGS=( "$@" )
 
-KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
+KUBERNETES_MODE=${KUBERNETES_MODE:="git_mode"}
+START_KUBERNETES_CLUSTER=${START_KUBERNETES_CLUSTER:="false"}
 
-if [[ "${KUBERNETES_VERSION}" == "" ]]; then
+if [[ "${START_KUBERNETES_CLUSTER}" == "false" ]]; then
     echo "Initializing the DB"
     yes | airflow db init || true
     airflow db reset -y
@@ -52,7 +53,8 @@ RES=$?
 set +x
 if [[ "${RES}" != "0" ]]; then
     if [[ -f "${XUNIT_FILE:=}" ]]; then
-        SEPARATOR_WIDTH=${SEPARATOR_WIDTH=$(tput cols)}
+        SEPARATOR_WIDTH=${SEPARATOR_WIDTH:=$(tput cols 2>/dev/null)}
+        SEPARATOR_WIDTH=${SEPARATOR_WIDTH:="120"}
         echo
         printf '=%.0s' $(seq "${SEPARATOR_WIDTH}")
         echo
diff --git a/scripts/ci/in_container/run_docs_build.sh b/scripts/ci/in_container/run_docs_build.sh
index a1c5e7a9700a4..697153ea7a039 100755
--- a/scripts/ci/in_container/run_docs_build.sh
+++ b/scripts/ci/in_container/run_docs_build.sh
@@ -30,8 +30,14 @@ in_container_script_start
 
 sudo rm -rf "$(pwd)/docs/_build/*"
 sudo rm -rf "$(pwd)/docs/_api/*"
+
+pushd "$(pwd)/airflow/www/static" || exit
+ln -sf ../../../docs/_build/html docs
+popd || exit
+
 sudo -E "$(pwd)/docs/build.sh"
 
+
 in_container_fix_ownership
 
 in_container_script_end
diff --git a/scripts/ci/in_container/run_extract_tests.sh b/scripts/ci/in_container/run_extract_tests.sh
index 75bef099cc048..1327bffc559d5 100755
--- a/scripts/ci/in_container/run_extract_tests.sh
+++ b/scripts/ci/in_container/run_extract_tests.sh
@@ -16,7 +16,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-#
 # Bash sanity settings (error on exit, complain for undefined vars, error when pipe fails)
 set -euo pipefail
 
@@ -33,6 +32,6 @@ TMP_FILE=$(mktemp)
 
 nosetests --collect-only --with-xunit --xunit-file="${TMP_FILE}"
 
-python "${AIRFLOW_SOURCES}/tests/test_utils/get_all_tests.py" "${TMP_FILE}" | sort >> "${HOME}/all_tests.txt"
+    python "${AIRFLOW_SOURCES}/tests/test_utils/get_all_tests.py" "${TMP_FILE}" | sort >> "${HOME}/all_tests.txt"
 
 in_container_script_end
diff --git a/scripts/ci/local_ci_fix_ownership.sh b/scripts/ci/in_container/run_isort.sh
similarity index 50%
rename from scripts/ci/local_ci_fix_ownership.sh
rename to scripts/ci/in_container/run_isort.sh
index 0f6e6027738ca..489bb0a7efec0 100755
--- a/scripts/ci/local_ci_fix_ownership.sh
+++ b/scripts/ci/in_container/run_isort.sh
@@ -16,34 +16,41 @@
 # specific language governing permissions and limitations
 # under the License.
 
-#
-# Fixes ownership for files created inside container (files owned by root will be owned by host user)
-#
-
-set -euo pipefail
-MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+# Script to run flake8 on all code. Can be started from any working directory
+set -uo pipefail
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+MY_DIR=$(cd "$(dirname "$0")" || exit 1; pwd)
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/in_container/_in_container_utils.sh
+. "${MY_DIR}/_in_container_utils.sh"
 
-script_start
+in_container_basic_sanity_check
 
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
+in_container_script_start
 
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
-${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
+if [[ ${#@} == "0" ]]; then
+    print_in_container_info
+    print_in_container_info "Running isort with no parameters"
+    print_in_container_info
+else
+    print_in_container_info
+    print_in_container_info "Running isort with parameters: $*"
+    print_in_container_info
+fi
 
-HOST_USER_ID="$(id -ur)"
-export HOST_USER_ID
+isort "$@"
 
-HOST_GROUP_ID="$(id -gr)"
-export HOST_GROUP_ID
+RES="$?"
 
-docker-compose \
-    -f "${MY_DIR}/docker-compose.yml" \
-    -f "${MY_DIR}/docker-compose-local.yml" \
-    run --no-deps airflow-testing /opt/airflow/scripts/ci/in_container/run_fix_ownership.sh
+in_container_script_end
 
-script_end
+if [[ "${RES}" != 0 ]]; then
+    echo >&2
+    echo >&2 "There were some isort errors. Exiting"
+    echo >&2
+    exit 1
+else
+    echo
+    echo "Isort succeeded"
+    echo
+fi
diff --git a/scripts/ci/in_container/run_pylint_main.sh b/scripts/ci/in_container/run_pylint_main.sh
index 198636ca8d1f1..e95e3aefb982c 100755
--- a/scripts/ci/in_container/run_pylint_main.sh
+++ b/scripts/ci/in_container/run_pylint_main.sh
@@ -56,7 +56,7 @@ else
     print_in_container_info "Running Pylint with parameters: $*"
     print_in_container_info
     echo "PATH=${PATH}"
-    /usr/local/bin/pylint --output-format=colorized "$@"
+    pylint --output-format=colorized "$@"
     RES=$?
 fi
 
diff --git a/scripts/ci/kubernetes/app/deploy_app.sh b/scripts/ci/kubernetes/app/deploy_app.sh
deleted file mode 100755
index 9e1337b1c85b8..0000000000000
--- a/scripts/ci/kubernetes/app/deploy_app.sh
+++ /dev/null
@@ -1,236 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-set -x
-
-export AIRFLOW_IMAGE=${IMAGE:-airflow}
-export AIRFLOW_TAG=${TAG:-latest}
-DIRNAME=$(cd "$(dirname "$0")" || exit 1; pwd)
-export DIRNAME
-export TEMPLATE_DIRNAME="${DIRNAME}/templates"
-export BUILD_DIRNAME="${DIRNAME}/build"
-AIRFLOW_SOURCES=$(cd "${DIRNAME}/../../../../" || exit 1 ; pwd)
-export AIRFLOW_SOURCES
-
-# shellcheck source=common/_autodetect_variables.sh
-. "${AIRFLOW_SOURCES}/common/_autodetect_variables.sh"
-
-# Source branch will be set in DockerHub
-SOURCE_BRANCH=${SOURCE_BRANCH:=${DEFAULT_BRANCH}}
-# if AIRFLOW_CONTAINER_BRANCH_NAME is not set it will be set to either SOURCE_BRANCH (if overridden)
-# or default branch for the sources
-AIRFLOW_CONTAINER_BRANCH_NAME=${AIRFLOW_CONTAINER_BRANCH_NAME:=${SOURCE_BRANCH}}
-
-usage() {
-    cat << EOF
-  usage: $0 options
-  OPTIONS:
-    -d Use PersistentVolume or GitSync for dags_folder. Available options are "persistent_mode" or "git_mode"
-EOF
-    exit 1;
-}
-
-while getopts ":d:" OPTION; do
-  case ${OPTION} in
-    d)
-      DAGS_VOLUME=${OPTARG};;
-    \?)
-      echo "Invalid option: -$OPTARG" >&2
-      exit 1
-      ;;
-    :)
-      echo "Option -$OPTARG requires an argument." >&2
-      usage
-      ;;
-  esac
-done
-
-case ${DAGS_VOLUME} in
-  "persistent_mode")
-    GIT_SYNC="0"
-    ;;
-  "git_mode")
-    GIT_SYNC="1"
-    ;;
-  *)
-    echo "Value \"$DAGS_VOLUME\" for dags_folder is not valid." >&2
-    usage
-    ;;
-esac
-
-if [[ ! -d "${BUILD_DIRNAME}" ]]; then
-  mkdir -p "${BUILD_DIRNAME}"
-fi
-
-rm -f "${BUILD_DIRNAME}"/*
-
-if [[ "${GIT_SYNC}" == "0" ]]; then
-    INIT_DAGS_VOLUME_NAME=airflow-dags
-    POD_AIRFLOW_DAGS_VOLUME_NAME=airflow-dags
-    CONFIGMAP_DAGS_FOLDER=/root/airflow/dags
-    CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT=
-    CONFIGMAP_DAGS_VOLUME_CLAIM=airflow-dags
-else
-    INIT_DAGS_VOLUME_NAME=airflow-dags-fake
-    POD_AIRFLOW_DAGS_VOLUME_NAME=airflow-dags-git
-    CONFIGMAP_DAGS_FOLDER=/root/airflow/dags/repo/airflow/contrib/example_dags
-    CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT=/root/airflow/dags
-    CONFIGMAP_DAGS_VOLUME_CLAIM=
-fi
-CONFIGMAP_GIT_REPO=${TRAVIS_REPO_SLUG:-apache/airflow}
-CONFIGMAP_BRANCH=${DEFAULT_BRANCH:=master}
-
-_UNAME_OUT=$(uname -s)
-case "${_UNAME_OUT}" in
-    Linux*)     _MY_OS=linux;;
-    Darwin*)    _MY_OS=darwin;;
-    *)          echo "${_UNAME_OUT} is unsupported."
-                exit 1;;
-esac
-echo "Local OS is ${_MY_OS}"
-
-case ${_MY_OS} in
-  linux)
-    SED_COMMAND="sed"
-  ;;
-  darwin)
-    SED_COMMAND="gsed"
-    if ! type "${SED_COMMAND}" &> /dev/null ; then
-      echo "Could not find \"${SED_COMMAND}\" binary, please install it. On OSX brew install gnu-sed" >&2
-      exit 1
-    fi
-  ;;
-  *)
-    echo "${_UNAME_OUT} is unsupported."
-    exit 1
-  ;;
-esac
-
-if [[ "${GIT_SYNC}" == "0" ]]; then
-  ${SED_COMMAND} -e "s/{{INIT_GIT_SYNC}}//g" \
-      "${TEMPLATE_DIRNAME}/airflow.template.yaml" > "${BUILD_DIRNAME}/airflow.yaml"
-else
-  ${SED_COMMAND} -e "/{{INIT_GIT_SYNC}}/{r ${TEMPLATE_DIRNAME}/init_git_sync.template.yaml" -e 'd}' \
-      "${TEMPLATE_DIRNAME}/airflow.template.yaml" > "${BUILD_DIRNAME}/airflow.yaml"
-fi
-${SED_COMMAND} -i "s|{{AIRFLOW_IMAGE}}|${AIRFLOW_IMAGE}|g" "${BUILD_DIRNAME}/airflow.yaml"
-${SED_COMMAND} -i "s|{{AIRFLOW_TAG}}|${AIRFLOW_TAG}|g" "${BUILD_DIRNAME}/airflow.yaml"
-
-${SED_COMMAND} -i "s|{{CONFIGMAP_GIT_REPO}}|${CONFIGMAP_GIT_REPO}|g" "${BUILD_DIRNAME}/airflow.yaml"
-${SED_COMMAND} -i "s|{{CONFIGMAP_BRANCH}}|${CONFIGMAP_BRANCH}|g" "${BUILD_DIRNAME}/airflow.yaml"
-${SED_COMMAND} -i "s|{{INIT_DAGS_VOLUME_NAME}}|${INIT_DAGS_VOLUME_NAME}|g" "${BUILD_DIRNAME}/airflow.yaml"
-${SED_COMMAND} -i "s|{{POD_AIRFLOW_DAGS_VOLUME_NAME}}|${POD_AIRFLOW_DAGS_VOLUME_NAME}|g" \
-    "${BUILD_DIRNAME}/airflow.yaml"
-
-${SED_COMMAND} "s|{{CONFIGMAP_DAGS_FOLDER}}|${CONFIGMAP_DAGS_FOLDER}|g" \
-    "${TEMPLATE_DIRNAME}/configmaps.template.yaml" > "${BUILD_DIRNAME}/configmaps.yaml"
-${SED_COMMAND} -i "s|{{CONFIGMAP_GIT_REPO}}|${CONFIGMAP_GIT_REPO}|g" "${BUILD_DIRNAME}/configmaps.yaml"
-${SED_COMMAND} -i "s|{{CONFIGMAP_BRANCH}}|${CONFIGMAP_BRANCH}|g" "${BUILD_DIRNAME}/configmaps.yaml"
-${SED_COMMAND} -i "s|{{CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT}}|${CONFIGMAP_GIT_DAGS_FOLDER_MOUNT_POINT}|g" \
-    "${BUILD_DIRNAME}/configmaps.yaml"
-${SED_COMMAND} -i "s|{{CONFIGMAP_DAGS_VOLUME_CLAIM}}|${CONFIGMAP_DAGS_VOLUME_CLAIM}|g" \
-    "${BUILD_DIRNAME}/configmaps.yaml"
-
-
-cat "${BUILD_DIRNAME}/airflow.yaml"
-cat "${BUILD_DIRNAME}/configmaps.yaml"
-
-kubectl delete -f "${DIRNAME}/postgres.yaml"
-kubectl delete -f "${BUILD_DIRNAME}/airflow.yaml"
-kubectl delete -f "${DIRNAME}/secrets.yaml"
-
-set -e
-
-kubectl apply -f "${DIRNAME}/secrets.yaml"
-kubectl apply -f "${BUILD_DIRNAME}/configmaps.yaml"
-kubectl apply -f "${DIRNAME}/postgres.yaml"
-kubectl apply -f "${DIRNAME}/volumes.yaml"
-kubectl apply -f "${BUILD_DIRNAME}/airflow.yaml"
-
-dump_logs() {
-  echo "------- pod description -------"
-  kubectl describe pod "${POD}"
-  echo "------- webserver init container logs - init -------"
-  kubectl logs "${POD}" -c init || true
-  if [[ "${GIT_SYNC}" == "1" ]]; then
-      echo "------- webserver init container logs - git-sync-clone -------"
-      kubectl logs "${POD}" -c git-sync-clone || true
-  fi
-  echo "------- webserver logs -------"
-  kubectl logs "${POD}" -c webserver || true
-  echo "------- scheduler logs -------"
-  kubectl logs "${POD}" -c scheduler || true
-  echo "--------------"
-}
-
-
-set +x
-# wait for up to 10 minutes for everything to be deployed
-PODS_ARE_READY="0"
-for i in {1..150}
-do
-  echo "------- Running kubectl get pods: $i -------"
-  PODS=$(kubectl get pods | awk 'NR>1 {print $0}')
-  echo "$PODS"
-  NUM_AIRFLOW_READY=$(echo "${PODS}" | grep airflow | awk '{print $2}' | grep -cE '([0-9])\/(\1)' | xargs)
-  NUM_POSTGRES_READY=$(echo "${PODS}" | grep postgres | awk '{print $2}' | grep -cE '([0-9])\/(\1)' | xargs)
-  if [[ "${NUM_AIRFLOW_READY}" == "1" && "${NUM_POSTGRES_READY}" == "1" ]]; then
-    PODS_ARE_READY="1"
-    break
-  fi
-  sleep 4
-done
-POD=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}' | grep airflow | head -1)
-
-if [[ "${PODS_ARE_READY}" == "1" ]]; then
-  echo "PODS are ready."
-else
-  echo >&2 "PODS are not ready after waiting for a long time. Exiting..."
-  dump_logs
-  exit 1
-fi
-
-# Wait until Airflow webserver is up
-KUBERNETES_HOST=docker
-AIRFLOW_WEBSERVER_IS_READY="0"
-CONSECUTIVE_SUCCESS_CALLS=0
-for i in {1..30}
-do
-  echo "------- Wait until webserver is up: $i -------"
-  HTTP_CODE=$(curl -LI "http://${KUBERNETES_HOST}:30809/health" -o /dev/null -w '%{http_code}\n' -sS) || true
-  if [[ "${HTTP_CODE}" == 200 ]]; then
-    (( CONSECUTIVE_SUCCESS_CALLS+=1 ))
-  else
-    CONSECUTIVE_SUCCESS_CALLS="0"
-  fi
-  if [[ "${CONSECUTIVE_SUCCESS_CALLS}" == 3 ]]; then
-    AIRFLOW_WEBSERVER_IS_READY="1"
-    break
-  fi
-  sleep 10
-done
-
-if [[ "${AIRFLOW_WEBSERVER_IS_READY}" == "1" ]]; then
-  echo "Airflow webserver is ready."
-else
-  echo >&2 "Airflow webserver is not ready after waiting for a long time. Exiting..."
-  dump_logs
-  exit 1
-fi
-
-dump_logs
diff --git a/scripts/ci/kubernetes/docker/build.sh b/scripts/ci/kubernetes/docker/build.sh
deleted file mode 100755
index 8f11bb1aadd1f..0000000000000
--- a/scripts/ci/kubernetes/docker/build.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-IMAGE=${IMAGE:-airflow}
-TAG=${TAG:-latest}
-DIRNAME=$(cd "$(dirname "$0")" && pwd)
-AIRFLOW_ROOT="${DIRNAME}/../../../.."
-
-set -e
-
-echo "Airflow directory ${AIRFLOW_ROOT}"
-echo "Airflow Docker directory ${DIRNAME}"
-
-cd "${DIRNAME}" && docker build --build-arg AIRFLOW_CI_IMAGE="${AIRFLOW_CONTAINER_DOCKER_IMAGE}" --pull "${DIRNAME}" --tag="${IMAGE}:${TAG}"
-
-kind load docker-image "${IMAGE}:${TAG}"
diff --git a/scripts/ci/kubernetes/setup_kubernetes.sh b/scripts/ci/kubernetes/setup_kubernetes.sh
deleted file mode 100755
index d24d53a4bcbda..0000000000000
--- a/scripts/ci/kubernetes/setup_kubernetes.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-set -euo pipefail
-DIRNAME=$(cd "$(dirname "$0")" && pwd)
-
-# start kubernetes
-kind delete cluster || true
-kind create cluster --config "${DIRNAME}/kind-cluster-conf.yaml"
-mv "$(kind get kubeconfig-path)" ~/.kube/config
-kubectl config set clusters.kind.server https://docker:19090
-kubectl cluster-info
-
-kubectl get nodes
-echo "Showing storageClass"
-kubectl get storageclass
-echo "Showing kube-system pods"
-kubectl get -n kube-system pods
-
-"${DIRNAME}/docker/build.sh"
-
-echo "Airflow environment on kubernetes is good to go!"
diff --git a/scripts/ci/local_ci_build.sh b/scripts/ci/local_ci_build.sh
index 08c0f70c3d967..771a76ab22717 100755
--- a/scripts/ci/local_ci_build.sh
+++ b/scripts/ci/local_ci_build.sh
@@ -23,13 +23,21 @@ set -euo pipefail
 set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
+
+# Set default python version
+export PYTHON_VERSION=${PYTHON_VERSION:=${DEFAULT_PYTHON_VERSION}}
 
 script_start
 
+initialize_environment
+
+prepare_build
+
 rebuild_all_images_if_needed_and_confirmed
 
 script_end
diff --git a/scripts/ci/local_ci_build_ci_image.sh b/scripts/ci/local_ci_build_ci_image.sh
index b632f0009e2cf..a110a226145c0 100755
--- a/scripts/ci/local_ci_build_ci_image.sh
+++ b/scripts/ci/local_ci_build_ci_image.sh
@@ -16,20 +16,21 @@
 # specific language governing permissions and limitations
 # under the License.
 
-set -euo pipefail
-#
-# Builds full CI docker image - the image that can be used for running full tests of Airflow
-#
 set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
+initialize_environment
+
+prepare_build
+
 rebuild_ci_image_if_needed
 
 script_end
diff --git a/scripts/ci/local_ci_build_ci_slim_image.sh b/scripts/ci/local_ci_build_ci_slim_image.sh
deleted file mode 100755
index b632f0009e2cf..0000000000000
--- a/scripts/ci/local_ci_build_ci_slim_image.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-set -euo pipefail
-#
-# Builds full CI docker image - the image that can be used for running full tests of Airflow
-#
-set -euo pipefail
-MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
-
-basic_sanity_checks
-
-script_start
-
-rebuild_ci_image_if_needed
-
-script_end
diff --git a/scripts/ci/local_ci_cleanup.sh b/scripts/ci/local_ci_cleanup.sh
index 2595e7f54c032..213ee8b8fe80c 100755
--- a/scripts/ci/local_ci_cleanup.sh
+++ b/scripts/ci/local_ci_cleanup.sh
@@ -23,13 +23,18 @@ set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
+initialize_environment
+
+prepare_build
+
 cleanup_ci_image
 
 script_end
diff --git a/scripts/ci/local_ci_enter_environment.sh b/scripts/ci/local_ci_enter_environment.sh
index d6d524940ab78..c2b6de72f4bf8 100755
--- a/scripts/ci/local_ci_enter_environment.sh
+++ b/scripts/ci/local_ci_enter_environment.sh
@@ -20,8 +20,7 @@ set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export RUN_TESTS="false"
-export MOUNT_LOCAL_SOURCES="true"
-export PYTHON_VERSION=${PYTHON_VERSION:="3.6"}
+export MOUNT_HOST_VOLUMES="true"
 export VERBOSE=${VERBOSE:="false"}
 
 echo
diff --git a/scripts/ci/local_ci_pull_and_build.sh b/scripts/ci/local_ci_pull_and_build.sh
index c7ee6a33f2b00..b92f939a5d5e4 100755
--- a/scripts/ci/local_ci_pull_and_build.sh
+++ b/scripts/ci/local_ci_pull_and_build.sh
@@ -23,16 +23,17 @@ set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
 script_start
 
-export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="true"
-export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
-export AIRFLOW_CONTAINER_SKIP_LATEST_PYTHON_PULL="false"
+export FORCE_PULL_IMAGES="true"
+export FORCE_DOCKER_BUILD="true"
+export PULL_BASE_IMAGES="true"
 
 rebuild_all_images_if_needed_and_confirmed
 
diff --git a/scripts/ci/local_ci_run_airflow_testing.sh b/scripts/ci/local_ci_run_airflow_testing.sh
index c71d3c857d13e..997078ad34d90 100755
--- a/scripts/ci/local_ci_run_airflow_testing.sh
+++ b/scripts/ci/local_ci_run_airflow_testing.sh
@@ -20,7 +20,7 @@ set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
 export RUN_TESTS="true"
-export MOUNT_LOCAL_SOURCES="true"
+export MOUNT_HOST_VOLUMES="true"
 export VERBOSE=${VERBOSE:="false"}
 
 # shellcheck source=./ci_run_airflow_testing.sh
diff --git a/scripts/ci/local_ci_stop_environment.sh b/scripts/ci/local_ci_stop_environment.sh
index 823cd0d507c99..94caa7a37a97c 100755
--- a/scripts/ci/local_ci_stop_environment.sh
+++ b/scripts/ci/local_ci_stop_environment.sh
@@ -22,20 +22,20 @@
 set -euo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 
-# shellcheck source=scripts/ci/_utils.sh
-. "${MY_DIR}/_utils.sh"
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
 
-basic_sanity_checks
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
 
-script_start
+# Set default python version
+export PYTHON_VERSION=${PYTHON_VERSION:=${DEFAULT_PYTHON_VERSION}}
 
-export PYTHON_VERSION=${PYTHON_VERSION:="3.6"}
+script_start
 
-# Default branch name for triggered builds is master
-export AIRFLOW_CONTAINER_BRANCH_NAME=${AIRFLOW_CONTAINER_BRANCH_NAME:="master"}
+initialize_environment
 
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
-${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
+prepare_build
 
 HOST_USER_ID="$(id -ur)"
 export HOST_USER_ID
diff --git a/scripts/ci/pre_commit_check_license.sh b/scripts/ci/pre_commit_check_license.sh
index bf63c18d1c2fd..d659299d9c47a 100755
--- a/scripts/ci/pre_commit_check_license.sh
+++ b/scripts/ci/pre_commit_check_license.sh
@@ -21,5 +21,6 @@ set -uo pipefail
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 export FORCE_ANSWER_TO_QUESTIONS=${FORCE_ANSWER_TO_QUESTIONS:="quit"}
 export REMEMBER_LAST_ANSWER="true"
+export MOUNT_HOST_VOLUMES="true"
 
 "${MY_DIR}/ci_check_license.sh"
diff --git a/scripts/ci/pre_commit_ci_build.sh b/scripts/ci/pre_commit_ci_build.sh
index 628a0e448613a..38aa12a2d0456 100755
--- a/scripts/ci/pre_commit_ci_build.sh
+++ b/scripts/ci/pre_commit_ci_build.sh
@@ -20,6 +20,5 @@ set -uo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 export REMEMBER_LAST_ANSWER="false"
-export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
 
 "${MY_DIR}/local_ci_build.sh"
diff --git a/scripts/ci/pre_commit_pylint_main.sh b/scripts/ci/pre_commit_pylint_main.sh
index 70f496fe91fbc..a4025d53606d6 100755
--- a/scripts/ci/pre_commit_pylint_main.sh
+++ b/scripts/ci/pre_commit_pylint_main.sh
@@ -16,10 +16,28 @@
 # specific language governing permissions and limitations
 # under the License.
 
-set -uo pipefail
+set -euo pipefail
 
 MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-export FORCE_ANSWER_TO_QUESTIONS=${FORCE_ANSWER_TO_QUESTIONS:="quit"}
-export REMEMBER_LAST_ANSWER="true"
 
-"${MY_DIR}/ci_pylint_main.sh" "${@}"
+export AIRFLOW_CI_SILENT=${AIRFLOW_CI_SILENT:="true"}
+
+AIRFLOW_SOURCES="$(cd "${MY_DIR}"/../../ && pwd )"
+export AIRFLOW_SOURCES
+
+# shellcheck source=scripts/ci/utils/_include_all.sh
+. "${MY_DIR}/utils/_include_all.sh"
+
+script_start
+
+initialize_environment
+
+prepare_build
+
+prepare_run
+
+rebuild_ci_image_if_needed
+
+run_isort "$@"
+
+script_end
diff --git a/scripts/ci/utils/_build.sh b/scripts/ci/utils/_build.sh
new file mode 100644
index 0000000000000..6b0ef7181fb83
--- /dev/null
+++ b/scripts/ci/utils/_build.sh
@@ -0,0 +1,773 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Assume AIRFLOW_SOURCES are set to point to sources of Airflow
+
+#
+# Sets file name for last forced answer. This is where last forced answer is stored. It is usually
+# removed by "forget_last_answer" method just before question is asked. The only exceptions
+# are pre-commit checks which (except the first - build - step) do not remove the last forced
+# answer so that the first answer is used for all builds within this pre-commit run.
+#
+function _set_last_force_answer_file() {
+    LAST_FORCE_ANSWER_FILE="${BUILD_CACHE_DIR}/last_force_answer.sh"
+    export LAST_FORCE_ANSWER_FILE
+}
+
+function _match_files_regexp() {
+    FILE_MATCHES="false"
+    REGEXP=${1}
+    while (($#))
+    do
+        REGEXP=${1}
+        for FILE in ${CHANGED_FILE_NAMES}
+        do
+          if  [[ ${FILE} =~ ${REGEXP} ]]; then
+             FILE_MATCHES="true"
+          fi
+        done
+        shift
+    done
+    export FILE_MATCHES
+}
+
+# Removes the "Force answer" (yes/no/quit) given previously
+#
+# This is the default behaviour of all rebuild scripts to ask independently whether you want to
+# rebuild the image or not. Sometimes however we want to remember answer previously given.
+# This is for example if you answered "no" to rebuild the image, the assumption is that you do not
+# want to rebuild image for other rebuilds in the same pre-commit execution.
+#
+# All the pre-commit checks therefore have `export REMEMBER_LAST_ANSWER="true"` set
+# So that in case they are run in a sequence of commits they will not rebuild. Similarly if your most
+# recent answer was "no" and you run `pre-commit run mypy` (for example) it will also reuse the
+# "no" answer given previously.
+#
+# This happens until you run any of the breeze commands or run build stepo of pre-commit.
+#
+function forget_last_answer() {
+    if [[ ${REMEMBER_LAST_ANSWER:=""} != "true" ]]; then
+        print_info
+        print_info "Removing last answer from ${LAST_FORCE_ANSWER_FILE}"
+        print_info
+        rm -f "${LAST_FORCE_ANSWER_FILE}"
+    else
+        if [[ -f "${LAST_FORCE_ANSWER_FILE}" ]]; then
+            print_info
+            print_info "Retaining last answer from ${LAST_FORCE_ANSWER_FILE}"
+            print_info "$(cat "${LAST_FORCE_ANSWER_FILE}")"
+            print_info
+        fi
+    fi
+}
+
+#
+# Sets mounting of host volumes to container for static checks
+# unless MOUNT_HOST_VOLUMES is not true
+#
+# Output: EXTRA_DOCKER_FLAGS
+function _set_extra_container_docker_flags() {
+    MOUNT_HOST_VOLUMES=${MOUNT_HOST_VOLUMES:="true"}
+    export MOUNT_HOST_VOLUMES
+
+    if [[ ${MOUNT_HOST_VOLUMES} == "true" ]]; then
+        print_info
+        print_info "Mounting host volumes to Docker"
+        print_info
+        if [[ -d "${AIRFLOW_SOURCES}/.bash_history" ]]; then
+            rm -rf "${AIRFLOW_SOURCES}/.bash_history"
+        fi
+        touch "${AIRFLOW_SOURCES}/.bash_history"
+        EXTRA_DOCKER_FLAGS=( \
+          "-v" "${AIRFLOW_SOURCES}/airflow:/opt/airflow/airflow:cached" \
+          "-v" "${AIRFLOW_SOURCES}/common:/opt/airflow/common:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.mypy_cache:/opt/airflow/.mypy_cache:cached" \
+          "-v" "${AIRFLOW_SOURCES}/dev:/opt/airflow/dev:cached" \
+          "-v" "${AIRFLOW_SOURCES}/docs:/opt/airflow/docs:cached" \
+          "-v" "${AIRFLOW_SOURCES}/scripts:/opt/airflow/scripts:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.kube:/root/.kube:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.bash_history:/root/.bash_history:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.bash_aliases:/root/.bash_aliases:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.inputrc:/root/.inputrc:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.bash_completion.d:/root/.bash_completion.d:cached" \
+          "-v" "${AIRFLOW_SOURCES}/tmp:/opt/airflow/tmp:cached" \
+          "-v" "${AIRFLOW_SOURCES}/tests:/opt/airflow/tests:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.flake8:/opt/airflow/.flake8:cached" \
+          "-v" "${AIRFLOW_SOURCES}/pylintrc:/opt/airflow/pylintrc:cached" \
+          "-v" "${AIRFLOW_SOURCES}/setup.cfg:/opt/airflow/setup.cfg:cached" \
+          "-v" "${AIRFLOW_SOURCES}/setup.py:/opt/airflow/setup.py:cached" \
+          "-v" "${AIRFLOW_SOURCES}/.rat-excludes:/opt/airflow/.rat-excludes:cached" \
+          "-v" "${AIRFLOW_SOURCES}/logs:/opt/airflow/logs:cached" \
+          "-v" "${AIRFLOW_SOURCES}/logs:/root/logs:cached" \
+          "-v" "${AIRFLOW_SOURCES}/files:/files:cached" \
+          "-v" "${AIRFLOW_SOURCES}/tmp:/opt/airflow/tmp:cached" \
+          "--env" "PYTHONDONTWRITEBYTECODE" \
+        )
+    else
+        print_info
+        print_info "Skip mounting host volumes to Docker"
+        print_info
+        EXTRA_DOCKER_FLAGS=( \
+            "--env" "PYTHONDONTWRITEBYTECODE" \
+        )
+    fi
+    export EXTRA_DOCKER_FLAGS
+}
+
+#
+# Verifies if stored md5sum of the file changed since the last tme ot was checked
+# The md5sum files are stored in .build directory - you can delete this directory
+# If you want to rebuild everything from the scratch
+# Returns 0 if md5sum is OK
+function _check_file_md5sum {
+    local FILE="${1}"
+    local MD5SUM
+    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${THE_IMAGE_TYPE}"
+    mkdir -pv "${MD5SUM_CACHE_DIR}"
+    MD5SUM=$(md5sum "${FILE}")
+    local MD5SUM_FILE
+    MD5SUM_FILE="${MD5SUM_CACHE_DIR}"/$(basename "${FILE}").md5sum
+    local MD5SUM_FILE_NEW
+    MD5SUM_FILE_NEW=${CACHE_TMP_FILE_DIR}/$(basename "${FILE}").md5sum.new
+    echo "${MD5SUM}" > "${MD5SUM_FILE_NEW}"
+    local RET_CODE=0
+    if [[ ! -f "${MD5SUM_FILE}" ]]; then
+        print_info "Missing md5sum for ${FILE#${AIRFLOW_SOURCES}} (${MD5SUM_FILE#${AIRFLOW_SOURCES}})"
+        RET_CODE=1
+    else
+        diff "${MD5SUM_FILE_NEW}" "${MD5SUM_FILE}" >/dev/null
+        RES=$?
+        if [[ "${RES}" != "0" ]]; then
+            print_info "The md5sum changed for ${FILE}"
+            RET_CODE=1
+        fi
+    fi
+    return ${RET_CODE}
+}
+
+#
+# Moves md5sum file from it's temporary location in CACHE_TMP_FILE_DIR to
+# BUILD_CACHE_DIR - thus updating stored MD5 sum fo the file
+#
+function _move_file_md5sum {
+    local FILE="${1}"
+    local MD5SUM_FILE
+    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${THE_IMAGE_TYPE}"
+    mkdir -pv "${MD5SUM_CACHE_DIR}"
+    MD5SUM_FILE="${MD5SUM_CACHE_DIR}"/$(basename "${FILE}").md5sum
+    local MD5SUM_FILE_NEW
+    MD5SUM_FILE_NEW=${CACHE_TMP_FILE_DIR}/$(basename "${FILE}").md5sum.new
+    if [[ -f "${MD5SUM_FILE_NEW}" ]]; then
+        mv "${MD5SUM_FILE_NEW}" "${MD5SUM_FILE}"
+        print_info "Updated md5sum file ${MD5SUM_FILE} for ${FILE}."
+    fi
+}
+
+#
+# Stores md5sum files for all important files and
+# records that we built the images locally so that next time we use
+# it from the local docker cache rather than pull (unless forced)
+#
+function _update_all_md5_files() {
+    print_info
+    print_info "Updating md5sum files"
+    print_info
+    local FILE
+    for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"
+    do
+        _move_file_md5sum "${AIRFLOW_SOURCES}/${FILE}"
+    done
+    local SUFFIX=""
+    if [[ -n ${PYTHON_VERSION:=""} ]]; then
+        SUFFIX="_${PYTHON_VERSION}"
+    fi
+    mkdir -pv "${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}"
+    touch "${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/.built_${THE_IMAGE_TYPE}${SUFFIX}"
+}
+
+#
+# Checks md5sum of all important files in order to optimise speed of running various operations
+# That mount sources of Airflow to container and require docker image built with latest dependencies.
+# the Docker image will only be marked for rebuilding only in case any of the important files change:
+#
+# This is needed because we want to skip rebuilding of the image when only airflow sources change but
+# Trigger rebuild in case we need to change dependencies (setup.py, setup.cfg, change version of Airflow
+# or the Dockerfile itself changes.
+#
+# As result of this check - most of the static checks will start pretty much immediately.
+#
+# Output:  DOCKER_BUILD_NEEDED
+#
+function _check_if_docker_build_is_needed() {
+    print_info "Checking if build is needed for ${THE_IMAGE_TYPE} image python version: ${PYTHON_VERSION}"
+    local IMAGE_BUILD_NEEDED="false"
+    local FILE
+    if [[ ${FORCE_DOCKER_BUILD:=""} == "true" ]]; then
+        print_info "Docker image build is forced for ${THE_IMAGE_TYPE} image"
+        set +e
+        for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"
+        do
+            # Just store md5sum for all files in md5sum.new - do not check if it is different
+            _check_file_md5sum "${AIRFLOW_SOURCES}/${FILE}"
+        done
+        set -e
+        IMAGES_TO_REBUILD+=("${THE_IMAGE_TYPE}")
+        export DOCKER_BUILD_NEEDED="true"
+    else
+        set +e
+        for FILE in "${FILES_FOR_REBUILD_CHECK[@]}"
+        do
+            if ! _check_file_md5sum "${AIRFLOW_SOURCES}/${FILE}"; then
+                export DOCKER_BUILD_NEEDED="true"
+                IMAGE_BUILD_NEEDED=true
+            fi
+        done
+        set -e
+        if [[ ${IMAGE_BUILD_NEEDED} == "true" ]]; then
+            IMAGES_TO_REBUILD+=("${THE_IMAGE_TYPE}")
+            export DOCKER_BUILD_NEEDED="true"
+            print_info "Docker image build is needed for ${THE_IMAGE_TYPE} image!"
+        else
+            print_info "Docker image build is not needed for ${THE_IMAGE_TYPE} image!"
+        fi
+    fi
+    print_info
+}
+
+#
+# Confirms that the image should be rebuilt
+# Input LAST_FORCE_ANSWER_FILE - file containing FORCE_ANSWER_TO_QUESTIONS variable set
+# Output: FORCE_ANSWER_TO_QUESTIONS (yes/no)
+#         LAST_FORCE_ANSWER_FILE contains `export FORCE_ANSWER_TO_QUESTIONS=yes/no`
+# Exits when q is selected.
+function _confirm_image_rebuild() {
+    if [[ -f "${LAST_FORCE_ANSWER_FILE}" ]]; then
+        # set variable from last answered response given in the same pre-commit run - so that it can be
+        # set in one pre-commit check (build) and then used in another (pylint/mypy/flake8 etc).
+        # shellcheck disable=SC1090
+        source "${LAST_FORCE_ANSWER_FILE}"
+    fi
+    set +e
+    local RES
+    if [[ ${CI:="false"} == "true" ]]; then
+        print_info
+        print_info "CI environment - forcing rebuild for image ${THE_IMAGE_TYPE}."
+        print_info
+        RES="0"
+    elif [[ -n "${FORCE_ANSWER_TO_QUESTIONS:=""}" ]]; then
+        print_info
+        print_info "Forcing answer '${FORCE_ANSWER_TO_QUESTIONS}'"
+        print_info
+        case "${FORCE_ANSWER_TO_QUESTIONS}" in
+            [yY][eE][sS]|[yY])
+                RES="0" ;;
+            [qQ][uU][iI][tT]|[qQ])
+                RES="2" ;;
+            *)
+                RES="1" ;;
+        esac
+    elif [[ -t 0 ]]; then
+        # Check if this script is run interactively with stdin open and terminal attached
+        "${AIRFLOW_SOURCES}/confirm" "Rebuild image ${THE_IMAGE_TYPE} (might take some time)"
+        RES=$?
+    elif [[ ${DETECTED_TERMINAL:=$(tty)} != "not a tty" ]]; then
+        # Make sure to use output of tty rather than stdin/stdout when available - this way confirm
+        # will works also in case of pre-commits (git does not pass stdin/stdout to pre-commit hooks)
+        # shellcheck disable=SC2094
+        "${AIRFLOW_SOURCES}/confirm" "Rebuild image ${THE_IMAGE_TYPE} (might take some time)" \
+            <"${DETECTED_TERMINAL}" >"${DETECTED_TERMINAL}"
+        RES=$?
+        export DETECTED_TERMINAL
+    elif [[ -c /dev/tty ]]; then
+        export DETECTED_TERMINAL=/dev/tty
+        # Make sure to use /dev/tty first rather than stdin/stdout when available - this way confirm
+        # will works also in case of pre-commits (git does not pass stdin/stdout to pre-commit hooks)
+        # shellcheck disable=SC2094
+        "${AIRFLOW_SOURCES}/confirm" "Rebuild image ${THE_IMAGE_TYPE} (might take some time)" \
+            <"${DETECTED_TERMINAL}" >"${DETECTED_TERMINAL}"
+        RES=$?
+    else
+        print_info
+        print_info "No terminal, no stdin - quitting"
+        print_info
+        # No terminal, no stdin, no force answer - quitting!
+        RES="2"
+    fi
+    set -e
+    if [[ ${RES} == "1" ]]; then
+        print_info
+        print_info "Skipping rebuild for image ${THE_IMAGE_TYPE}"
+        print_info
+        SKIP_REBUILD="true"
+        # Force "no" also to subsequent questions so that if you answer it once, you are not asked
+        # For all other pre-commits and you will continue using the images you already have
+        export FORCE_ANSWER_TO_QUESTIONS="no"
+        echo 'export FORCE_ANSWER_TO_QUESTIONS="no"' > "${LAST_FORCE_ANSWER_FILE}"
+    elif [[ ${RES} == "2" ]]; then
+        echo >&2
+        echo >&2 "ERROR: The ${THE_IMAGE_TYPE} needs to be rebuilt - it is outdated. "
+        echo >&2 "   Make sure you build the images bu running run one of:"
+        echo >&2 "         * PYTHON_VERSION=${PYTHON_VERSION} ./scripts/ci/local_ci_build*.sh"
+        echo >&2 "         * PYTHON_VERSION=${PYTHON_VERSION} ./scripts/ci/local_ci_pull_and_build*.sh"
+        echo >&2
+        echo >&2 "   If you run it via pre-commit as individual hook, you can run 'pre-commit run build'."
+        echo >&2
+        exit 1
+    else
+        # Force "yes" also to subsequent questions
+        export FORCE_ANSWER_TO_QUESTIONS="yes"
+    fi
+}
+
+function pull_base_python_image() {
+    echo
+    verbose_docker pull "${PYTHON_BASE_IMAGE}"
+    echo
+}
+
+function _pull_image_if_needed() {
+    # Whether to force pull images to populate cache
+    export FORCE_PULL_IMAGES=${FORCE_PULL_IMAGES:="false"}
+    # In CI environment we skip pulling latest python image
+    export PULL_BASE_IMAGES=${PULL_BASE_IMAGES:=${NON_CI}}
+
+    if [[ "${USE_DOCKER_CACHE}" == "true" ]]; then
+        if [[ "${FORCE_PULL_IMAGES}" == "true" ]]; then
+            if [[ ${PULL_BASE_IMAGES} == "false" ]]; then
+                echo
+                echo "Skip force-pulling the ${PYTHON_BASE_IMAGE} image."
+                echo
+            else
+                echo
+                echo "Force pull base image ${PYTHON_BASE_IMAGE}"
+                echo
+                verbose_docker pull "${PYTHON_BASE_IMAGE}"
+                echo
+            fi
+        fi
+        IMAGES="${AIRFLOW_IMAGE}"
+        for IMAGE in ${IMAGES}
+        do
+            local PULL_IMAGE=${FORCE_PULL_IMAGES}
+            local IMAGE_HASH
+            IMAGE_HASH=$(docker images -q "${IMAGE}" 2> /dev/null)
+            if [[ "${IMAGE_HASH}" == "" ]]; then
+                PULL_IMAGE="true"
+            fi
+            if [[ "${PULL_IMAGE}" == "true" ]]; then
+                echo
+                echo "Pulling the image ${IMAGE}"
+                echo
+                verbose_docker pull "${IMAGE}" || true
+                echo
+            fi
+        done
+    fi
+}
+
+#
+# Builds the image
+#
+function _build_image() {
+    _print_build_info
+    echo
+    echo Building image "${IMAGE_DESCRIPTION}"
+    echo
+    _pull_image_if_needed
+
+    if [[ "${USE_LOCAL_DOCKER_CACHE}" == "true" ]]; then
+        export DOCKER_CACHE_CI_DIRECTIVE=()
+    elif [[ "${USE_DOCKER_CACHE}" == "false" ]]; then
+        export DOCKER_CACHE_CI_DIRECTIVE=("--no-cache")
+    else
+        export DOCKER_CACHE_CI_DIRECTIVE=(
+            "--cache-from" "${AIRFLOW_CI_IMAGE}"
+        )
+    fi
+    VERBOSE=${VERBOSE:="false"}
+    if [[ -n ${DETECTED_TERMINAL:=""} ]]; then
+        echo -n "Building ${THE_IMAGE_TYPE}.
+        " > "${DETECTED_TERMINAL}"
+        spin "${OUTPUT_LOG}" &
+        SPIN_PID=$!
+        # shellcheck disable=SC2064
+        trap "kill ${SPIN_PID}" SIGINT SIGTERM
+    fi
+    if [[ ${THE_IMAGE_TYPE} == "CI" ]]; then
+        DEBUG_FIXING_PERMISSIONS=${DEBUG_FIXING_PERMISSIONS:="false"}
+        set +u
+        verbose_docker build \
+            --build-arg PYTHON_BASE_IMAGE="${PYTHON_BASE_IMAGE}" \
+            --build-arg AIRFLOW_VERSION="${AIRFLOW_VERSION}" \
+            --build-arg AIRFLOW_BRANCH="${BRANCH_NAME}" \
+            --build-arg DEBUG_FIXING_PERMISSIONS="${DEBUG_FIXING_PERMISSIONS}" \
+            "${DOCKER_CACHE_CI_DIRECTIVE[@]}" \
+            -t "${AIRFLOW_CI_IMAGE}" \
+            --target "${TARGET_IMAGE}" \
+            . | tee -a "${OUTPUT_LOG}"
+        set -u
+    fi
+    if [[ -n "${DEFAULT_IMAGE:=}" ]]; then
+        verbose_docker tag "${AIRFLOW_IMAGE}" "${DEFAULT_IMAGE}" | tee -a "${OUTPUT_LOG}"
+    fi
+    if [[ -n ${SPIN_PID:=""} ]]; then
+        kill "${SPIN_PID}" || true
+        wait "${SPIN_PID}" || true
+        echo > "${DETECTED_TERMINAL}"
+    fi
+}
+
+function remove_all_images() {
+    echo
+    "${AIRFLOW_SOURCES}/confirm" "Removing all local images ."
+    echo
+    verbose_docker rmi "${PYTHON_BASE_IMAGE}" || true
+    verbose_docker rmi "${AIRFLOW_CI_IMAGE}" || true
+    echo
+    echo "###################################################################"
+    echo "NOTE!! Removed Airflow images for Python version ${PYTHON_VERSION}."
+    echo "       But the disk space in docker will be reclaimed only after"
+    echo "       running 'docker system prune' command."
+    echo "###################################################################"
+    echo
+}
+
+#
+# Rebuilds an image if needed
+#
+function _rebuild_image_if_needed() {
+    set_image_variables
+
+    if [[ -f "${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/.built_${THE_IMAGE_TYPE}_${PYTHON_VERSION}" ]]; then
+        print_info
+        print_info "${THE_IMAGE_TYPE} image already built locally."
+        print_info
+    else
+        print_info
+        print_info "${THE_IMAGE_TYPE} image not built locally: force building"
+        print_info
+        export FORCE_DOCKER_BUILD="true"
+    fi
+
+    DOCKER_BUILD_NEEDED="false"
+    IMAGES_TO_REBUILD=()
+    _check_if_docker_build_is_needed
+    if [[ "${DOCKER_BUILD_NEEDED}" == "true" ]]; then
+        SKIP_REBUILD="false"
+        if [[ ${CI:=} != "true" && "${FORCE_BUILD:=}" != "true" ]]; then
+            _confirm_image_rebuild
+        fi
+        if [[ ${SKIP_REBUILD} != "true" ]]; then
+            _fix_group_permissions
+            print_info
+            print_info "Rebuilding started: ${THE_IMAGE_TYPE} image."
+            print_info
+            _build_image
+            _update_all_md5_files
+            print_info
+            print_info "Rebuilding completed: ${THE_IMAGE_TYPE} image."
+            print_info
+        fi
+    else
+        print_info
+        print_info "No need to rebuild - none of the important files changed: ${FILES_FOR_REBUILD_CHECK[*]}"
+        print_info
+    fi
+}
+
+_cleanup_image() {
+    set_image_variables
+    verbose_docker rmi "${AIRFLOW_IMAGE}" || true
+}
+
+_push_image() {
+    set_image_variables
+    verbose_docker push "${AIRFLOW_IMAGE}"
+    if [[ -n ${DEFAULT_IMAGE:=""} ]]; then
+        verbose_docker push "${DEFAULT_IMAGE}"
+    fi
+}
+
+function rebuild_ci_image_if_needed() {
+    export THE_IMAGE_TYPE="CI"
+    export IMAGE_DESCRIPTION="Airflow CI"
+    export TARGET_IMAGE="airflow-ci"
+    _rebuild_image_if_needed
+    if [[ ${START_KUBERNETES_CLUSTER} == "true" || ! -f "${AIRFLOW_CI_KUBERNETES_IMAGE_ARCHIVE}" ]]; then
+        _build_and_save_kubernetes_image
+    fi
+}
+
+function cleanup_ci_image() {
+    export THE_IMAGE_TYPE="CI"
+    _cleanup_image
+}
+
+function push_ci_image() {
+    export THE_IMAGE_TYPE="CI"
+    _push_image
+}
+
+function _go_to_airflow_sources {
+    print_info
+    pushd "${AIRFLOW_SOURCES}" &>/dev/null || exit 1
+    print_info
+    print_info "Running in host in $(pwd)"
+    print_info
+}
+
+function rebuild_all_images_if_needed_and_confirmed() {
+    DOCKER_BUILD_NEEDED="false"
+    IMAGES_TO_REBUILD=()
+    for THE_IMAGE_TYPE in "${LOCALLY_BUILT_IMAGES[@]}"
+    do
+        _check_if_docker_build_is_needed
+    done
+
+    if [[ ${DOCKER_BUILD_NEEDED} == "true" ]]; then
+        print_info
+        print_info "Docker image build is needed for ${IMAGES_TO_REBUILD[*]}!"
+        print_info
+    else
+        print_info
+        print_info "Docker image build is not needed for any of the image types!"
+        print_info
+    fi
+
+    if [[ "${DOCKER_BUILD_NEEDED}" == "true" ]]; then
+        echo
+        echo "Some of your images need to be rebuild because important files (like package list) has changed."
+        echo
+        echo "You have those options:"
+        echo "   * Rebuild the images now by answering 'y' (this might take some time!)"
+        echo "   * Skip rebuilding the images and hope changes are not big (you will be asked again)"
+        echo "   * Quit and manually rebuild the images using"
+        echo "        * PYTHON_VERSION=${PYTHON_VERSION} scripts/local_ci_build.sh or"
+        echo "        * PYTHON_VERSION=${PYTHON_VERSION} scripts/local_ci_pull_and_build.sh or"
+        echo
+        export ACTION="rebuild"
+        export THE_IMAGE_TYPE="${IMAGES_TO_REBUILD[*]}"
+
+        SKIP_REBUILD="false"
+        _confirm_image_rebuild
+
+        if [[ ${SKIP_REBUILD} != "true" ]]; then
+            rebuild_ci_image_if_needed
+        fi
+    fi
+}
+
+function build_image_on_ci() {
+    if [[ "${CI:=}" != "true" ]]; then
+        print_info
+        print_info "Cleaning up docker installation!!!!!!"
+        print_info
+        "${AIRFLOW_SOURCES}/confirm" "Cleaning docker data and rebuilding"
+    fi
+
+    export FORCE_PULL_IMAGES="true"
+    export FORCE_BUILD="true"
+    export VERBOSE="${VERBOSE:="false"}"
+
+    # Cleanup docker installation. It should be empty in CI but let's not risk
+    verbose_docker system prune --all --force | tee -a "${OUTPUT_LOG}"
+    rm -rf "${BUILD_CACHE_DIR}"
+    mkdir -pv "${BUILD_CACHE_DIR}"
+
+    echo
+    echo "Finding changed file names ${TRAVIS_BRANCH}...HEAD"
+    echo
+    git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
+    git fetch origin "${TRAVIS_BRANCH}"
+    CHANGED_FILE_NAMES=$(git diff --name-only "${TRAVIS_BRANCH}...HEAD")
+    echo
+    echo "Changed file names in this commit"
+    echo "${CHANGED_FILE_NAMES}"
+    echo
+
+    if [[ ${TRAVIS_JOB_NAME} == "Tests"*"kubernetes"* ]]; then
+        _match_files_regexp 'airflow/kubernetes/.*\.py' 'tests/kubernetes/.*\.py' \
+            'airflow/www/.*\.py' 'airflow/www/.*\.js' 'airflow/www/.*\.html' \
+            'scripts/ci/.*'
+        if [[ ${FILE_MATCHES} == "true" || ${TRAVIS_PULL_REQUEST:=} == "false" ]]; then
+            rebuild_ci_image_if_needed
+        else
+            touch "${BUILD_CACHE_DIR}"/.skip_tests
+        fi
+    elif [[ ${TRAVIS_JOB_NAME:=""} == "Tests"* ]]; then
+        _match_files_regexp '.*\.py' 'airflow/www/.*\.py' 'airflow/www/.*\.js' 'airflow/www/.*\.html' \
+            'scripts/ci/.*'
+        if [[ ${FILE_MATCHES} == "true" || ${TRAVIS_PULL_REQUEST:=} == "false" ]]; then
+            rebuild_ci_image_if_needed
+        else
+            touch "${BUILD_CACHE_DIR}"/.skip_tests
+        fi
+    elif [[ ${TRAVIS_JOB_NAME} == "Pylint"* ]]; then
+        _match_files_regexp '.*\.py'
+        if [[ ${FILE_MATCHES} == "true" || ${TRAVIS_PULL_REQUEST:=} == "false" ]]; then
+            rebuild_ci_image_if_needed
+        else
+            touch "${BUILD_CACHE_DIR}"/.skip_tests
+        fi
+    else
+        rebuild_ci_image_if_needed
+    fi
+
+    if [[ -f "${BUILD_CACHE_DIR}/.skip_tests" ]]; then
+        echo
+        echo "Skip running tests !!!!"
+        echo
+    fi
+
+    # Disable force pulling forced above
+    unset FORCE_PULL_IMAGES
+    unset FORCE_BUILD
+}
+
+function _print_build_info() {
+    print_info
+    print_info "Airflow ${AIRFLOW_VERSION} Python: ${PYTHON_VERSION}."
+    print_info
+}
+
+# Clears the status of fixing permissions so that this is done only once per script execution
+function _clear_fix_group_permissions() {
+    unset PERMISSIONS_FIXED
+}
+#
+# Fixing permissions for all important files that are going to be added to Docker context
+# This is necessary, because there are different default umask settings on different *NIX
+# In case of some systems (especially in the CI environments) there is default +w group permission
+# set automatically via UMASK when git checkout is performed.
+#    https://unix.stackexchange.com/questions/315121/why-is-the-default-umask-002-or-022-in-many-unix-systems-seems-insecure-by-defa
+# Unfortunately default setting in git is to use UMASK by default:
+#    https://git-scm.com/docs/git-config/1.6.3.1#git-config-coresharedRepository
+# This messes around with Docker context invalidation because the same files have different permissions
+# and effectively different hash used for context validation calculation.
+#
+# We fix it by removing write permissions for other/group for important files that are checked during
+# building docker images
+#
+function _fix_group_permissions() {
+    if [[ ${PERMISSIONS_FIXED:=} == "true" ]]; then
+        echo
+        echo "Permissions already fixed"
+        echo
+        return
+    fi
+    echo
+    echo "Fixing group permissions"
+    # Fix permissions for all files (and directories of those files) that are not git-ignored.
+    # Create HTML dir for dangling symlink. Using git ls-files is fast way of getting
+    # all the relevant files and removing group permissions. Then we use the files to also
+    # walk up the path and fix all the directories. This way we avoid costly walking through
+    # all directories in the folder including node_modules and other git-ignored folders generated
+    # in static
+    pushd "${AIRFLOW_SOURCES}" >/dev/null
+    FILES=$(git ls-files)
+    FILE_NAME="files"
+    while [[ "${FILES}" != "." ]];
+    do
+        echo "${FILES}" | xargs chmod og-rw
+        echo "Fixing $(echo "${FILES}" | wc -l) ${FILE_NAME}"
+        FILES=$(echo "${FILES}" | xargs -n 1 dirname | sort | uniq)
+        FILE_NAME="directories"
+    done
+    echo
+    popd >/dev/null
+    echo "Fixed group permissions"
+    echo
+    export PERMISSIONS_FIXED="true"
+}
+
+function set_image_variables() {
+    export PYTHON_BASE_IMAGE="python:${PYTHON_VERSION}-slim-buster"
+
+    if [[ ${THE_IMAGE_TYPE:=} == "CI" ]]; then
+        export AIRFLOW_IMAGE="${AIRFLOW_CI_IMAGE}"
+        export AIRFLOW_IMAGE_DEFAULT="${AIRFLOW_CI_IMAGE_DEFAULT}"
+    else
+        export AIRFLOW_IMAGE=""
+        export AIRFLOW_IMAGE_DEFAULT=""
+    fi
+
+    if [[ "${PYTHON_VERSION_FOR_DEFAULT_IMAGE}" == "${PYTHON_VERSION}" ]]; then
+        export DEFAULT_IMAGE="${AIRFLOW_IMAGE_DEFAULT}"
+    else
+        export DEFAULT_IMAGE=""
+    fi
+}
+
+function _get_image_variables {
+    export AIRFLOW_CI_IMAGE="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${DEFAULT_BRANCH}-python${PYTHON_VERSION}-ci"
+    export AIRFLOW_CI_SAVED_IMAGE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}-python${PYTHON_VERSION}-ci-image"
+    export AIRFLOW_CI_IMAGE_ID_FILE="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}-python${PYTHON_VERSION}-ci-image.sha256"
+
+    echo
+    echo "Using CI image: ${AIRFLOW_CI_IMAGE} for docker compose runs"
+    echo "Saved docker image will be created here: ${AIRFLOW_CI_SAVED_IMAGE_DIR}"
+    echo "Id of the saved docker image will be here: ${AIRFLOW_CI_IMAGE_ID_FILE}"
+    echo
+
+}
+
+# Builds and saves Kubernetes image to a .tar file so that it can be mounted inside the container and
+# Loaded by kind inside the docker
+function _build_and_save_kubernetes_image() {
+
+    SAVED_AIRFLOW_IMAGE_ID=$(cat "${AIRFLOW_CI_IMAGE_ID_FILE}" 2>/dev/null || true)
+    export SAVED_AIRFLOW_IMAGE_ID
+
+    if [[ ${SAVED_AIRFLOW_IMAGE_ID} == "" ]]; then
+        echo
+        echo "This is the first time the image is saved"
+        echo
+    fi
+    AIRFLOW_IMAGE_ID=$(docker inspect --format='{{index .Id}}' "${AIRFLOW_CI_IMAGE}")
+    if [[ "${AIRFLOW_IMAGE_ID}" == "${SAVED_AIRFLOW_IMAGE_ID}" ]]; then
+        echo
+        echo "The image ${AIRFLOW_CI_IMAGE} has not changed since the last time, skiping saving."
+        echo
+    else
+        TMPDIR=$(mktemp -d)
+        echo
+        echo "Saving ${AIRFLOW_CI_IMAGE} image to ${AIRFLOW_CI_SAVED_IMAGE_DIR}"
+        echo
+        docker save "${AIRFLOW_CI_IMAGE}" | tar -xC "${TMPDIR}"
+        mkdir -pv "${AIRFLOW_CI_SAVED_IMAGE_DIR}"
+        rsync --recursive --checksum --whole-file --delete --stats --human-readable \
+            "${TMPDIR}/" "${AIRFLOW_CI_SAVED_IMAGE_DIR}"
+        rm -rf "${TMPDIR}"
+        echo
+        echo "Image ${AIRFLOW_CI_IMAGE} saved to ${AIRFLOW_CI_SAVED_IMAGE_DIR}"
+        echo
+        echo "${AIRFLOW_IMAGE_ID}" >"${AIRFLOW_CI_IMAGE_ID_FILE}"
+    fi
+}
+
+#
+# Performs basic sanity checks common for most of the scripts in this directory
+#
+function prepare_build() {
+    _set_last_force_answer_file
+    _get_image_variables
+    forget_last_answer
+    _go_to_airflow_sources
+    _set_extra_container_docker_flags
+    _clear_fix_group_permissions
+}
diff --git a/scripts/ci/utils/_common_variables.sh b/scripts/ci/utils/_common_variables.sh
new file mode 100644
index 0000000000000..6de460a65db85
--- /dev/null
+++ b/scripts/ci/utils/_common_variables.sh
@@ -0,0 +1,127 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+#
+# Autodetects common variables depending on the environment it is run in
+#
+# This autodetection of PYTHON_VERSION works in this sequence:
+##
+#    1) When running builds on Travis CI we use python version determined
+#       from default python3 available on the path. This way we
+#       do not have to specify PYTHON_VERSION separately in each job.
+#       (We do not need it it in Travis). We just specify which host python
+#       version is used for that job. This makes a nice UI experience where
+#       you see python version in Travis UI.
+#
+#    2) Running builds locally via scripts where we can pass PYTHON_VERSION
+#       as environment variable. If variable is not set we use the same detection
+#       as in Travis CI.
+#
+
+# Assume AIRFLOW_SOURCES are set to point to sources of Airflow
+
+# shellcheck source=scripts/ci/utils/_default_branch.sh
+. "${AIRFLOW_SOURCES}/scripts/ci/utils/_default_branch.sh"
+
+export DEFAULT_PYTHON_VERSION=3.6
+export STATIC_CHECK_PYTHON_VERSION=3.6
+
+# Sets up basic variables needed for the buils
+#  Output:
+#
+#  * DEFAULT_BRANCH                - default branch used (master/v1-10-test)
+#  * DOCKERHUB_USER/DOCKERHUB_REPO - DockerHub user/repo  (<USER>/<REPO>)
+#  * BRANCH_NAME                   - branch for the build
+#  * PYTHON_VERSION                - actually used PYTHON_VERSION (might be overwritten)
+#  * CI/NON_CI                     - ("true"/"false") to distinguish CI vs. NON_CI build
+#  * AIRFLOW_VERSION               - version of Airflow from sources
+#  * USE_DOCKER_CACHE              - set to false if CRON job detected
+#
+function _setup_basic_variables() {
+    # You can override DOCKERHUB_USER to use your own DockerHub account and play with your
+    # own docker images. In this case you can build images locally and push them
+    export DOCKERHUB_USER=${DOCKERHUB_USER:="apache"}
+
+    # You can override DOCKERHUB_REPO to use your own DockerHub repository and play with your
+    # own docker images. In this case you can build images locally and push them
+    export DOCKERHUB_REPO=${DOCKERHUB_REPO:="airflow"}
+
+    # if BRANCH_NAME is not set it will be set to either SOURCE_BRANCH
+    # (if overridden by configuration) or default branch configured in /common/_default_branch.sh
+    SOURCE_BRANCH=${SOURCE_BRANCH:=${DEFAULT_BRANCH}}
+
+    BRANCH_NAME=${BRANCH_NAME:=${SOURCE_BRANCH}}
+
+    echo
+    echo "Branch: ${BRANCH_NAME}"
+    echo
+
+    # Determine python version. This can be either specified by PYTHON_VERSION variable or it is
+    # Set to (default version)
+    export PYTHON_VERSION=${PYTHON_VERSION:=${DEFAULT_PYTHON_VERSION}}
+
+    IMAGE_TAG=${BRANCH_NAME}-python${PYTHON_VERSION}
+
+    # Extract TAG_PREFIX from IMAGE_TAG (master-python3.6 -> master)
+    TAG_PREFIX=${IMAGE_TAG%-*}
+    export TAG_PREFIX
+
+    # Disable writing .pyc files - slightly slower imports but not messing around when switching
+    # Python version and avoids problems with root-owned .pyc files in host
+    export PYTHONDONTWRITEBYTECODE=${PYTHONDONTWRITEBYTECODE:="true"}
+
+    # By default we assume the kubernetes cluster is not being started
+    export START_KUBERNETES_CLUSTER=${START_KUBERNETES_CLUSTER:="false"}
+
+    # Check if we are running in the CI environment
+    CI=${CI:="false"}
+
+    if [[ "${CI}" == "true" ]]; then
+        NON_CI="false"
+    else
+        NON_CI="true"
+    fi
+    export CI
+    export NON_CI
+
+    # Determine version of the Airflow from version.py
+    AIRFLOW_VERSION=$(cat airflow/version.py - << EOF | python
+print(version.replace("+",""))
+EOF
+)
+    export AIRFLOW_VERSION
+
+    # Should dependencies of docker compose be started - default is true
+    DEPS=${DEPS:="true"}
+    export DEPS
+
+
+    if [[ -n ${TRAVIS_EVENT_TYPE:=} ]]; then
+        echo
+        echo "Travis event type: ${TRAVIS_EVENT_TYPE}"
+        echo
+    fi
+
+    # In case of CRON jobs on Travis we run builds without cache
+    if [[ "${TRAVIS_EVENT_TYPE}" == "cron" ]]; then
+        echo
+        echo "Disabling cache for CRON jobs"
+        echo
+        export USE_DOCKER_CACHE="false"
+    fi
+}
diff --git a/common/_default_branch.sh b/scripts/ci/utils/_default_branch.sh
similarity index 100%
rename from common/_default_branch.sh
rename to scripts/ci/utils/_default_branch.sh
diff --git a/common/_files_for_rebuild_check.sh b/scripts/ci/utils/_include_all.sh
similarity index 69%
rename from common/_files_for_rebuild_check.sh
rename to scripts/ci/utils/_include_all.sh
index f6474e675a91c..a1eb9b0dbaf9d 100644
--- a/common/_files_for_rebuild_check.sh
+++ b/scripts/ci/utils/_include_all.sh
@@ -1,5 +1,4 @@
 #!/usr/bin/env bash
-
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -17,12 +16,11 @@
 # specific language governing permissions and limitations
 # under the License.
 
-FILES_FOR_REBUILD_CHECK=(
- "setup.py"
- "setup.cfg"
- "Dockerfile"
- ".dockerignore"
- "airflow/version.py"
- "airflow/www/package.json"
- "airflow/www/package-lock.json" )
-export FILES_FOR_REBUILD_CHECK
+# Assume AIRFLOW_SOURCES are set to point to sources of Airflow
+
+# shellcheck source=scripts/ci/utils/_init.sh
+. "${AIRFLOW_SOURCES}/scripts/ci/utils/_init.sh"
+# shellcheck source=scripts/ci/utils/_build.sh
+. "${AIRFLOW_SOURCES}/scripts/ci/utils/_build.sh"
+# shellcheck source=scripts/ci/utils/_run.sh
+. "${AIRFLOW_SOURCES}/scripts/ci/utils/_run.sh"
diff --git a/scripts/ci/utils/_init.sh b/scripts/ci/utils/_init.sh
new file mode 100644
index 0000000000000..c42a97c11c48e
--- /dev/null
+++ b/scripts/ci/utils/_init.sh
@@ -0,0 +1,321 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Assume AIRFLOW_SOURCES are set to point to sources of Airflow
+
+#
+# Sets verbosity of shell in case VERBOSE is true
+# Input: VERBOSE
+#
+function _set_verbosity() {
+    if [[ ${VERBOSE:=} == "true" ]]; then
+        set -x
+    else
+        set +x
+    fi
+}
+
+# Creates temp directories
+function _create_ignored_directories() {
+    mkdir -p "${AIRFLOW_SOURCES}/.mypy_cache"
+    mkdir -p "${AIRFLOW_SOURCES}/logs"
+    mkdir -p "${AIRFLOW_SOURCES}/tmp"
+}
+
+function _set_python_version_for_default_image() {
+   PYTHON_VERSION_FOR_DEFAULT_IMAGE="${PYTHON_VERSION_FOR_DEFAULT_IMAGE:=${DEFAULT_PYTHON_VERSION}}"
+   export PYTHON_VERSION_FOR_DEFAULT_IMAGE
+}
+#
+# Creates cache directory where we will keep temporary files needed for the build
+#
+# This directory will be automatically deleted when the script is killed or exists (via trap)
+# Unless SKIP_CACHE_DELETION variable is set. You can set this variable and then see
+# the output/files generated by the scripts in this directory.
+#
+# Most useful is out.log file in this directory - storing verbose output of the scripts.
+#
+# Output: CACHE_TMP_FILE_DIR, OUTPUT_LOG
+function _create_temp_cache_directory() {
+    CACHE_TMP_FILE_DIR=$(mktemp -d)
+    export CACHE_TMP_FILE_DIR
+
+    if [[ ${SKIP_CACHE_DELETION:=} != "true" ]]; then
+        trap 'rm -rf -- "${CACHE_TMP_FILE_DIR}"' INT TERM HUP
+    fi
+
+    OUTPUT_LOG="${CACHE_TMP_FILE_DIR}/out.log"
+    export OUTPUT_LOG
+}
+
+#
+# Removes temporary cache directory
+#
+function _remove_temp_cache_directory() {
+    if [[ -z "${CACHE_TMP_FILE_DIR}" ]]; then
+        rm -rf -- "${CACHE_TMP_FILE_DIR}"
+    fi
+}
+
+#
+# Sets up cache variable and creates the cache directory if needed
+#
+# Input: AIRFLOW_SOURCES
+# Output: BUILD_CACHE_DIR
+function _setup_cache_directory() {
+    BUILD_CACHE_DIR="${AIRFLOW_SOURCES}/.build"
+    export BUILD_CACHE_DIR
+    mkdir -p "${BUILD_CACHE_DIR}"
+}
+
+#
+# Sets up locally built images as array
+#
+# Output: LOCALLY_BUILT_IMAGES (array)
+function _setup_locally_build_images() {
+    LOCALLY_BUILT_IMAGES=("CI")
+    export LOCALLY_BUILT_IMAGES
+}
+
+#
+# Sets up files that should be checked for rebuilding
+# Output: FILES_FOR_REBUILD_CHECK (array)
+function _setup_files_for_rebuild(){
+    FILES_FOR_REBUILD_CHECK=(
+         "setup.py"
+         "setup.cfg"
+         "Dockerfile"
+         ".dockerignore"
+         "airflow/version.py"
+         "airflow/www/package.json"
+         "airflow/www/package-lock.json"
+    )
+    export FILES_FOR_REBUILD_CHECK
+}
+
+#
+# Sets up ports forwarded when entering the container
+# Output: WEBSERVER_HOST_PORT, POSTGRES_HOST_PORT, MYSQL_HOST_PORT
+#
+function _setup_forwarded_ports() {
+    export WEBSERVER_HOST_PORT=${WEBSERVER_HOST_PORT:="28080"}
+    export POSTGRES_HOST_PORT=${POSTGRES_HOST_PORT:="25433"}
+    export MYSQL_HOST_PORT=${MYSQL_HOST_PORT:="23306"}
+}
+
+#
+# Checks if cache is going to be used
+function _check_if_cache_used() {
+    # You can set USE_DOCKER_CACHE to false if you do not want to use standard Docker
+    # cache during build. This way you can test building everything from the scratch
+    USE_DOCKER_CACHE=${USE_DOCKER_CACHE:="true"}
+
+    # You can set USE_LOCAL_DOCKER_CACHE to true if you do not want to use
+    # pulled images at all but you rely on local cache
+    USE_LOCAL_DOCKER_CACHE=${USE_LOCAL_DOCKER_CACHE:="false"}
+}
+
+#
+# Checks if core utils required in the host system are installed and explain what needs to be done if not
+# exits if it's not
+function _check_if_coreutils_installed() {
+    set +e
+    getopt -T >/dev/null
+    local GETOPT_RETVAL=$?
+
+    if [[ $(uname -s) == 'Darwin' ]] ; then
+        command -v gstat >/dev/null
+        local STAT_PRESENT=$?
+    else
+        command -v stat >/dev/null
+        local STAT_PRESENT=$?
+    fi
+
+    command -v md5sum >/dev/null
+    local MD5SUM_PRESENT=$?
+
+    set -e
+
+    local CMDNAME
+    CMDNAME="$(basename -- "$0")"
+
+    ####################  Parsing options/arguments
+    if [[ ${GETOPT_RETVAL} != 4 || "${STAT_PRESENT}" != "0" || "${MD5SUM_PRESENT}" != "0" ]]; then
+        echo
+        if [[ $(uname -s) == 'Darwin' ]] ; then
+            echo >&2 "You are running ${CMDNAME} in OSX environment"
+            echo >&2 "And you need to install gnu commands"
+            echo >&2
+            echo >&2 "Run 'brew install gnu-getopt coreutils'"
+            echo >&2
+            echo >&2 "Then link the gnu-getopt to become default as suggested by brew."
+            echo >&2
+            echo >&2 "If you use bash, you should run this command:"
+            echo >&2
+            echo >&2 "echo 'export PATH=\"/usr/local/opt/gnu-getopt/bin:\$PATH\"' >> ~/.bash_profile"
+            echo >&2 ". ~/.bash_profile"
+            echo >&2
+            echo >&2 "If you use zsh, you should run this command:"
+            echo >&2
+            echo >&2 "echo 'export PATH=\"/usr/local/opt/gnu-getopt/bin:\$PATH\"' >> ~/.zprofile"
+            echo >&2 ". ~/.zprofile"
+            echo >&2
+            echo >&2 "Login and logout afterwards !!"
+            echo >&2
+            echo >&2 "After re-login, your PATH variable should start with \"/usr/local/opt/gnu-getopt/bin\""
+            echo >&2 "Your current path is ${PATH}"
+            echo >&2
+        else
+            echo >&2 "You do not have necessary tools in your path (getopt, stat, md5sum)."
+            echo >&2 "Please install latest/GNU version of getopt and coreutils."
+            echo >&2 "This can usually be done with 'apt install util-linux coreutils'"
+        fi
+        echo
+        exit 1
+    fi
+}
+
+#
+# Asserts that we are not inside of the container
+#
+function _assert_not_in_container() {
+    if [[ -f /.dockerenv && ! ${SKIP_DOCKER_CONTAINER_CHECK:="false"} == "true" ]]; then
+        echo >&2
+        echo >&2 "You are inside the Airflow docker container!"
+        echo >&2 "You should only run this script from the host."
+        echo >&2 "Learn more about how we develop and test airflow in:"
+        echo >&2 "https://github.com/apache/airflow/blob/master/CONTRIBUTING.md"
+        echo >&2
+        exit 1
+    fi
+}
+
+#
+# Prints message on the stdout if VERBOSE == True  AIRFLOW_CI_SILENT != true
+# Input: VERBOSE, AIRFLOW_CI_SILENT
+function print_info() {
+    if [[ ${VERBOSE:="false"} == "true" || ${AIRFLOW_CI_SILENT:="false"} != "true" ]]; then
+        echo "$@"
+    fi
+}
+
+#
+# Starts the script/ If VERBOSE variable is set to true, it enables verbose output of commands executed
+# Also prints some useful diagnostics information at start of the script
+#
+function script_start {
+    print_info
+    print_info "Running $(basename "$0")"
+    print_info
+    print_info
+    if [[ ${VERBOSE:=} == "true" ]]; then
+        print_info
+        print_info "Variable VERBOSE Set to \"true\""
+        print_info "You will see a lot of output"
+        print_info
+        set -x
+    else
+        print_info "You can increase verbosity by running 'export VERBOSE=\"true\""
+        if [[ ${SKIP_CACHE_DELETION:=} != "true" ]]; then
+            print_info "And skip deleting the output file with 'export SKIP_CACHE_DELETION=\"true\""
+        fi
+        print_info
+        set +x
+    fi
+    START_SCRIPT_TIME=$(date +%s)
+}
+
+#
+#
+# Disables verbosity in the script
+#
+function script_end {
+    if [[ ${VERBOSE:=} == "true" ]]; then
+        set +x
+    fi
+    END_SCRIPT_TIME=$(date +%s)
+    RUN_SCRIPT_TIME=$((END_SCRIPT_TIME-START_SCRIPT_TIME))
+    print_info
+    print_info "Finished the script $(basename "$0")"
+    print_info "It took ${RUN_SCRIPT_TIME} seconds"
+    print_info
+    _remove_temp_cache_directory
+}
+
+function verbose_docker() {
+    echo "docker" "${@}"
+    docker "${@}"
+}
+
+function spin() {
+    local FILE_TO_MONITOR=${1}
+    local SPIN=("-" "\\" "|" "/")
+    echo -n " Build log: ${FILE_TO_MONITOR} ${SPIN[0]}" > "${DETECTED_TERMINAL}"
+
+    while "true"
+    do
+      for i in "${SPIN[@]}"
+      do
+            echo -ne "\b$i" > "${DETECTED_TERMINAL}"
+            local LAST_FILE_SIZE
+            local FILE_SIZE
+            LAST_FILE_SIZE=$(set +e; wc -c "${FILE_TO_MONITOR}" 2>/dev/null | awk '{print $1}' || true)
+            FILE_SIZE=${LAST_FILE_SIZE}
+            while [[ "${LAST_FILE_SIZE}" == "${FILE_SIZE}" ]];
+            do
+                FILE_SIZE=$(set +e; wc -c "${FILE_TO_MONITOR}" 2>/dev/null | awk '{print $1}' || true)
+                sleep 0.2
+            done
+            LAST_FILE_SIZE=FILE_SIZE
+            sleep 0.2
+            if [[ ! -f "${FILE_TO_MONITOR}" ]]; then
+                exit
+            fi
+      done
+    done
+}
+
+_assert_not_in_container
+
+# shellcheck source=scripts/ci/utils/_common_variables.sh
+. "${AIRFLOW_SOURCES}/scripts/ci/utils/_common_variables.sh"
+
+#
+# Performs basic sanity checks common for most of the scripts in this directory
+#
+function initialize_environment() {
+    _set_verbosity
+    _set_python_version_for_default_image
+    _create_ignored_directories
+    _create_temp_cache_directory
+    _setup_basic_variables
+    _setup_cache_directory
+    _setup_locally_build_images
+    _setup_files_for_rebuild
+    _setup_forwarded_ports
+    _check_if_cache_used
+    _check_if_coreutils_installed
+
+    if [[ -f ${BUILD_CACHE_DIR}/.skip_tests ]]; then
+        echo
+        echo "Skipping running tests !!!!!"
+        echo
+        script_end
+        exit
+    fi
+}
diff --git a/scripts/ci/utils/_run.sh b/scripts/ci/utils/_run.sh
new file mode 100644
index 0000000000000..eaf5b2331cb1a
--- /dev/null
+++ b/scripts/ci/utils/_run.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Assume AIRFLOW_SOURCES are set to point to sources of Airflow
+
+function _run_ci_script_with_docker() {
+    local SCRIPT="$1"
+    shift
+    FILES=("$@")
+
+    if [[ "${#FILES[@]}" == "0" ]]; then
+        verbose_docker run "${EXTRA_DOCKER_FLAGS[@]}" \
+            --entrypoint "/root/.local/bin/dumb-init"  \
+            --env PYTHONDONTWRITEBYTECODE \
+            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
+            --env AIRFLOW_CI_SILENT \
+            --env HOST_USER_ID="$(id -ur)" \
+            --env HOST_GROUP_ID="$(id -gr)" \
+            --rm \
+            "${AIRFLOW_CI_IMAGE}" \
+            "--" "${SCRIPT}" \
+            | tee -a "${OUTPUT_LOG}"
+    else
+        verbose_docker run "${EXTRA_DOCKER_FLAGS[@]}" \
+            --entrypoint "/root/.local/bin/dumb-init"  \
+            --env PYTHONDONTWRITEBYTECODE \
+            --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
+            --env AIRFLOW_CI_SILENT \
+            --env HOST_USER_ID="$(id -ur)" \
+            --env HOST_GROUP_ID="$(id -gr)" \
+            --rm \
+            "${AIRFLOW_CI_IMAGE}" \
+            "--" "${SCRIPT}" "${FILES[@]}" \
+            | tee -a "${OUTPUT_LOG}"
+    fi
+}
+
+function run_flake8() {
+    _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_flake8.sh" "$@"
+}
+
+function run_isort() {
+    _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_isort.sh" "$@"
+}
+
+
+function run_docs() {
+    _run_ci_script_with_docker "/opt/airflow/docs/build.sh"
+}
+
+function run_check_license() {
+    _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_check_licence.sh"
+}
+
+function run_mypy() {
+    FILES=("$@")
+    if [[ "${#FILES[@]}" == "0" ]]; then
+        _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_mypy.sh" "airflow" "tests" "docs"
+    else
+        _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_mypy.sh" "$@"
+    fi
+}
+
+function run_pylint_main() {
+    _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_pylint_main.sh" "$@"
+}
+
+
+function run_pylint_tests() {
+    _run_ci_script_with_docker "/opt/airflow/scripts/ci/in_container/run_pylint_tests.sh" "$@"
+}
+
+function run_docker_lint() {
+    FILES=("$@")
+    if [[ "${#FILES[@]}" == "0" ]]; then
+        echo
+        echo "Running docker lint for all Dockerfiles"
+        echo
+        verbose_docker run \
+            -v "$(pwd):/root" \
+            -w /root \
+            --rm \
+            hadolint/hadolint /bin/hadolint Dockerfile* | tee -a "${OUTPUT_LOG}"
+        echo
+        echo "Docker pylint completed with no errors"
+        echo
+    else
+        echo
+        echo "Running docker lint for $*"
+        echo
+        verbose_docker run \
+            -v "$(pwd):/root" \
+            -w /root \
+            --rm \
+            hadolint/hadolint /bin/hadolint "$@" | tee -a "${OUTPUT_LOG}"
+        echo
+        echo "Docker pylint completed with no errors"
+        echo
+    fi
+}
+
+function filter_out_files_from_pylint_todo_list() {
+  FILTERED_FILES=()
+  set +e
+  for FILE in "$@"
+  do
+      if [[ ${FILE} == "airflow/migrations/versions/"* ]]; then
+          # Skip all generated migration scripts
+          continue
+      fi
+      if ! grep -x "./${FILE}" <"${AIRFLOW_SOURCES}/scripts/ci/pylint_todo.txt" >/dev/null; then
+          FILTERED_FILES+=("${FILE}")
+      fi
+  done
+  set -e
+  export FILTERED_FILES
+}
+
+function refresh_pylint_todo() {
+    verbose_docker run "${EXTRA_DOCKER_FLAGS[@]}" \
+        --entrypoint /opt/airflow/scripts/ci/in_container/refresh_pylint_todo.sh \
+        --env PYTHONDONTWRITEBYTECODE \
+        --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
+        --env AIRFLOW_CI_SILENT \
+        --env HOST_USER_ID="$(id -ur)" \
+        --env HOST_GROUP_ID="$(id -gr)" \
+        --rm \
+        "${AIRFLOW_CI_IMAGE}" | tee -a "${OUTPUT_LOG}"
+}
+
+
+function prepare_run() {
+    _create_temp_cache_directory
+}
diff --git a/setup.py b/setup.py
index 43cee253ae3a1..2d6edcbfe66f5 100644
--- a/setup.py
+++ b/setup.py
@@ -237,6 +237,7 @@ def write_version(filename: str = os.path.join(*["airflow", "git_version"])):
 kubernetes = ['kubernetes>=3.0.0',
               'cryptography>=2.0.0']
 ldap = ['ldap3>=2.5.1']
+mongo = ['pymongo>=3.6.0', 'dnspython>=1.13.0,<2.0.0']
 mssql = ['pymssql~=2.1.1']
 mysql = ['mysqlclient>=1.3.6,<1.4']
 oracle = ['cx_Oracle>=5.1.2']
@@ -248,6 +249,7 @@ def write_version(filename: str = os.path.join(*["airflow", "git_version"])):
 ]
 pinot = ['pinotdb==0.1.1']
 postgres = ['psycopg2-binary>=2.7.4']
+pagerduty = ['pypd>=1.1.0']
 qds = ['qds-sdk>=1.10.4']
 rabbitmq = ['librabbitmq>=1.6.1']
 redis = ['redis~=3.2']
@@ -257,8 +259,6 @@ def write_version(filename: str = os.path.join(*["airflow", "git_version"])):
 sendgrid = ['sendgrid>=5.2.0,<6']
 sentry = ['sentry-sdk>=0.8.0', "blinker>=1.1"]
 slack = ['slackclient>=1.0.0,<2.0.0']
-pagerduty = ['pypd>=1.1.0']
-mongo = ['pymongo>=3.6.0', 'dnspython>=1.13.0,<2.0.0']
 snowflake = ['snowflake-connector-python>=1.5.2',
              'snowflake-sqlalchemy>=1.1.0']
 ssh = ['paramiko>=2.1.1', 'pysftp>=0.2.9', 'sshtunnel>=0.1.4,<0.2']
@@ -280,7 +280,6 @@ def write_version(filename: str = os.path.join(*["airflow", "git_version"])):
     'astroid~=2.2.5',  # to be removed after pylint solves this: https://github.com/PyCQA/pylint/issues/3123
     'beautifulsoup4~=4.7.1',
     'click==6.7',
-    'contextdecorator;python_version<"3.4"',
     'coverage',
     'dumb-init>=1.2.2',
     'flake8>=3.6.0',
@@ -290,47 +289,76 @@ def write_version(filename: str = os.path.join(*["airflow", "git_version"])):
     'jira',
     'mongomock',
     'moto==1.3.5',
+    'mypy==0.720',
     'nose',
     'nose-ignore-docstring==0.2',
     'nose-timer',
     'parameterized',
-    'paramiko',
     'pre-commit',
     'pylint~=2.3.1',  # to be upgraded after fixing https://github.com/PyCQA/pylint/issues/3123
                       # We should also disable checking docstring at the module level
-    'pysftp',
-    'pywinrm',
-    'qds-sdk>=1.9.6',
     'rednose',
     'requests_mock',
     'yamllint'
 ]
+
+devel = sorted(devel + doc)
+
 ############################################################################################################
 # IMPORTANT NOTE!!!!!!!!!!!!!!!
 # IF you are removing dependencies from the above list, please make sure that you also increase
 # DEPENDENCIES_EPOCH_NUMBER in the Dockerfile
 ############################################################################################################
 
-if PY3:
-    devel += ['mypy==0.720']
-else:
-    devel += ['unittest2']
-
 devel_minreq = devel + kubernetes + mysql + doc + password + cgroups
 devel_hadoop = devel_minreq + hive + hdfs + webhdfs + kerberos
-devel_all = (sendgrid + devel + all_dbs + doc + samba + slack + oracle +
-             docker + ssh + kubernetes + celery + redis + gcp + grpc +
-             datadog + zendesk + jdbc + ldap + kerberos + password + webhdfs + jenkins +
-             druid + pinot + segment + snowflake + elasticsearch + sentry +
-             atlas + azure + aws + salesforce + cgroups + papermill + virtualenv +
-             pagerduty)
 
-# Snakebite & Google Cloud Dataflow are not Python 3 compatible :'(
-if PY3:
-    devel_ci = [package for package in devel_all if package not in
-                ['snakebite>=2.7.8', 'snakebite[kerberos]>=2.7.8']]
-else:
-    devel_ci = devel_all
+all_packages = (
+    async_packages +
+    atlas +
+    all_dbs +
+    aws +
+    azure +
+    celery +
+    cgroups +
+    datadog +
+    dask +
+    databricks +
+    datadog +
+    docker +
+    druid +
+    elasticsearch +
+    gcp +
+    grpc +
+    flask_oauth +
+    jdbc +
+    jenkins +
+    kerberos +
+    kubernetes +
+    ldap +
+    oracle +
+    pagerduty +
+    papermill +
+    password +
+    pinot +
+    redis +
+    salesforce +
+    samba +
+    sendgrid +
+    sentry +
+    segment +
+    slack +
+    snowflake +
+    ssh +
+    statsd +
+    virtualenv +
+    webhdfs +
+    winrm +
+    zendesk
+)
+
+# Snakebite is not Python 3 compatible :'(
+all_packages = [package for package in all_packages if not package.startswith('snakebite')]
 
 
 def do_setup():
@@ -393,7 +421,6 @@ def do_setup():
             'tenacity==4.12.0',
             'termcolor==1.1.0',
             'text-unidecode==1.2',
-            'typing;python_version<"3.6"',
             'thrift>=0.9.2',
             'tzlocal>=1.4,<2.0.0',
             'unicodecsv>=0.14.1',
@@ -410,8 +437,7 @@ def do_setup():
             'gitpython>=2.0.2',
         ],
         extras_require={
-            'all': devel_all,
-            'devel_ci': devel_ci,
+            'all': all_packages,
             'all_dbs': all_dbs,
             'atlas': atlas,
             'async': async_packages,
@@ -424,7 +450,8 @@ def do_setup():
             'dask': dask,
             'databricks': databricks,
             'datadog': datadog,
-            'devel': devel_minreq,
+            'devel': devel,
+            'devel_minreq': devel_minreq,
             'devel_hadoop': devel_hadoop,
             'doc': doc,
             'docker': docker,
diff --git a/tests/core.py b/tests/core.py
index 5ff43dc9fe3be..05afc7603e512 100644
--- a/tests/core.py
+++ b/tests/core.py
@@ -97,7 +97,7 @@ def setUp(self):
         self.run_this_last = self.dag_bash.get_task('run_this_last')
 
     def tearDown(self):
-        if os.environ.get('KUBERNETES_VERSION') is not None:
+        if os.environ.get('START_KUBERNETES_CLUSTER') == "true":
             return
 
         dag_ids_to_clean = [
diff --git a/tests/integration/kubernetes/test_kubernetes_executor.py b/tests/integration/kubernetes/test_kubernetes_executor.py
index c335f16f0de11..21142501384b1 100644
--- a/tests/integration/kubernetes/test_kubernetes_executor.py
+++ b/tests/integration/kubernetes/test_kubernetes_executor.py
@@ -30,7 +30,7 @@
 try:
     check_call(["/usr/local/bin/kubectl", "get", "pods"])
 except Exception as e:  # pylint: disable=broad-except
-    if os.environ.get('KUBERNETES_VERSION'):
+    if os.environ.get('START_KUBERNETES_CLUSTER') == "true":
         raise e
     else:
         raise unittest.SkipTest(
diff --git a/tests/integration/kubernetes/test_kubernetes_pod_operator.py b/tests/integration/kubernetes/test_kubernetes_pod_operator.py
index 3a55f6a176fd4..0e82e832dc8fb 100644
--- a/tests/integration/kubernetes/test_kubernetes_pod_operator.py
+++ b/tests/integration/kubernetes/test_kubernetes_pod_operator.py
@@ -39,7 +39,7 @@
 try:
     check_call(["/usr/local/bin/kubectl", "get", "pods"])
 except Exception as e:  # pylint: disable=broad-except
-    if os.environ.get('KUBERNETES_VERSION'):
+    if os.environ.get('START_KUBERNETES_CLUSTER') == "true":
         raise e
     else:
         raise unittest.SkipTest(
diff --git a/tests/operators/test_operators.py b/tests/operators/test_operators.py
index e2612ccd1101c..b7e9e07b50844 100644
--- a/tests/operators/test_operators.py
+++ b/tests/operators/test_operators.py
@@ -78,8 +78,10 @@ def test_mysql_operator_test_multi(self):
         )
         t.run(start_date=DEFAULT_DATE, end_date=DEFAULT_DATE, ignore_ti_state=True)
 
-    @unittest.skipUnless('mysql' in configuration.conf.get('core', 'sql_alchemy_conn'),
-                         "This is a MySQL test")
+    @unittest.skip("""
+This tests is not working for mariadb as it uses not secure API of mysql
+That has been disabled in mariadb.
+    """)
     def test_mysql_hook_test_bulk_load(self):
         records = ("foo", "bar", "baz")
 
