diff --git a/.dockerignore b/.dockerignore
index 795f0842d6aea..e5bfaacf56ca9 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -39,12 +39,14 @@
 !.coveragerc
 !.rat-excludes
 !.flake8
+!.dockerignore
 !pylintrc
 !pytest.ini
+!CHANGELOG.txt
+!Dockerfile
 !LICENSE
 !MANIFEST.in
 !NOTICE
-!CHANGELOG.txt
 !.github
 
 # Avoid triggering context change on README change (new companies using Airflow)
@@ -54,16 +56,11 @@
 # Run tests command with bash completion
 !.bash_completion
 !.bash_completion.d
-!run-tests
-!run-tests-complete
 
 # Setup/version configuration
 !setup.cfg
 !setup.py
 
-# Entrypoint script
-!scripts/docker/entrypoint.sh
-
 # Now - ignore unnecessary files inside allowed directories
 # This goes after the allowed directories
 
diff --git a/.gitignore b/.gitignore
index 93add213c0fae..0e835da6593f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -143,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
@@ -173,5 +173,6 @@ dmypy.json
 /hive_scratch_dir/
 /.bash_aliases
 /.bash_history
+/.kube
 /.inputrc
 log.txt*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c60139eb30b28..07f074b02927a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -80,7 +80,7 @@ repos:
       - id: insert-license
         name: Add license for all shell files
         exclude: ^\.github/.*$"|^airflow/_vendor/.*$
-        files: ^breeze$|^breeze-complete$|\.sh$
+        files: ^breeze$|^breeze-complete$|\.sh$|\.bash$|\.bats$
         args:
           - --comment-style
           - "|#|"
@@ -168,8 +168,7 @@ repos:
         name: Check Shell scripts syntax correctness
         language: docker_image
         entry: koalaman/shellcheck:stable -x -a
-        types: [shell]
-        files: ^breeze$|^breeze-complete$|\.sh$|^hooks/build$|^hooks/push$
+        files: ^breeze$|^breeze-complete$|\.sh$|^hooks/build$|^hooks/push$|\.bash$|\.bats$
         exclude: ^airflow/_vendor/.*$
       - id: lint-dockerfile
         name: Lint dockerfile
@@ -285,6 +284,12 @@ repos:
         entry: "./scripts/ci/pre_commit_flake8.sh"
         files: \.py$
         pass_filenames: true
+      - id: bat-tests
+        name: Run BATS bash tests
+        language: system
+        entry: "./scripts/ci/pre_commit_bat_tests.sh"
+        files: ^tests/bats/.*\.bats$
+        pass_filenames: true
       - id: airflow-config-yaml
         name: Checks for consistency between config.yml and default_config.cfg
         language: python
diff --git a/.rat-excludes b/.rat-excludes
index b7a5b575378b3..08c17506eeeaa 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -31,6 +31,7 @@ CHANGELOG.txt
 .*lock
 unittests.cfg
 logs
+.bash_aliases
 
 # Generated doc files
 .*html
diff --git a/.travis.yml b/.travis.yml
index ee66370251a3c..d399033322a5b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -37,48 +37,47 @@ jobs:
       script: ./scripts/ci/ci_run_all_static_tests.sh
       env: >-
         PYTHON_VERSION=3.6
+        AIRFLOW_MOUNT_SOURCE_DIR_FOR_STATIC_CHECKS="true"
     - name: "Build documentation"
       env: >-
         PYTHON_VERSION=3.6
       stage: pre-test
       script: ./scripts/ci/ci_docs.sh
+    - name: "Tests postgres kubernetes python 3.6 (persistent)"
+      env: >-
+        BACKEND=postgres
+        PYTHON_VERSION=3.6
+        START_KIND_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
+        PYTHON_VERSION=3.6
+        START_KIND_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
       stage: test
     - name: "Tests sqlite python 3.6"
       env:
         BACKEND=sqlite
-        ENV=docker
         PYTHON_VERSION=3.6
       stage: test
     - name: "Tests mysql python 3.7"
       env:
         BACKEND=mysql
-        ENV=docker
         PYTHON_VERSION=3.7
       stage: test
-    - name: "Tests postgres kubernetes python 3.6 (persistent)"
-      env: >-
-        BACKEND=postgres
-        ENV=kubernetes
-        START_KUBERNETES_CLUSTER=true
-        KUBERNETES_VERSION=v1.15.0
-        KUBERNETES_MODE=persistent_mode
-        PYTHON_VERSION=3.6
-      stage: test
-      script: travis_wait 30 "./scripts/ci/ci_run_airflow_testing.sh"
-    - 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
-      stage: test
-      script: travis_wait 30 "./scripts/ci/ci_run_airflow_testing.sh"
   services:
     - docker
 before_install:
diff --git a/BREEZE.rst b/BREEZE.rst
index 7e397edd34cf2..3c317a0a05863 100644
--- a/BREEZE.rst
+++ b/BREEZE.rst
@@ -220,7 +220,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
@@ -229,25 +229,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
 ---------------------------
 
@@ -528,6 +509,54 @@ As soon as you enter the Breeze environment, you can run Airflow unit tests via
 
 For supported CI test suites, types of unit tests, and other tests, see `TESTING.rst <TESTING.rst>`_.
 
+Running Tests with Kubernetes in Breeze
+=======================================
+
+In order to run Kubernetes in Breeze you can start Breeze with ``--start-kind-cluster`` switch. This will
+automatically create a Kind Kubernetes cluster in a ``docker`` (docker-in-docker) container started as part
+of Breeze environment. Setting up the Kubernetes cluster takes some time so the cluster continues running
+until the environment is stopped with ``--stop-environment`` switch or until ``--recreate-kind-cluster``
+switch is used additionally to ``--start-kind-cluster``.
+
+The Kubernetes cluster is automatically initialized and starts Airflow webserver and scheduler
+using latest sources. This is done in two steps. First Airflow images are build in the ``docker`` image,
+then Airflow applications (``scheduler``/``webserver``) are prepared and deployed at the kind cluster using
+the images prepared.
+
+Rebuilding images should be faster every next time we enter the environment but it is attempted every time
+you restart breeze environment. Similarly applications are re-deployed every time the breeze environment
+is restarted.
+
+Building Airflow Images
+-----------------------
+
+This is done using ``scripts/ci/in_container/kubernetes/setup_kubernetes.sh`` script:
+
+1. Latest ``apache/airflow:master-pythonX.Y-ci`` images are pulled from DockerHub to the 'docker' container.
+2. The image is rebuild to take into account the latest sources mounted to Breeze environment.
+3. Scripts required for running the image as Kubernetes cluster are added and
+   ``apache/airflow:master-pythonX.Y-ci-kubernetes``
+4. The image is loaded to kind cluster using ``kind load`` command from kind.
+
+Deploying Airflow to Kind cluster
+---------------------------------
+
+This is done using ``scripts/ci/in_container/kubernetes/app/deploy_app.sh`` script:
+
+1. Kubernetes resources are prepared by processing template from ``template`` directory, replacing
+   variables with the right images and locations:
+   - configmaps.yaml
+   - airflow.yaml
+2. The existing resources are used without replacing any variables inside:
+   - secrets.yaml
+   - postgres.yaml
+   - volumes.yaml
+3. All the resources are applied in the Kind cluster
+4. The script will wait until all the applications are ready and reachable
+
+After the deployment is finished you can run Kubernetes tests immediately in the same way as other tests.
+The Kubernetes tests are in ``tests/integration/kubernetes`` folder.
+
 Breeze Command-Line Interface Reference
 =======================================
 
@@ -576,7 +605,7 @@ 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-apache-license check-executables-have-shebangs check-hooks-apply check-merge-conflict check-xml debug-statements doctoc detect-private-key end-of-file-fixer flake8 forbid-tabs insert-license lint-dockerfile mixed-line-ending mypy pylint pylint-test setup-order shellcheck].
+          [ all all-but-pylint bat-tests check-apache-license check-executables-have-shebangs check-hooks-apply check-merge-conflict check-xml debug-statements doctoc detect-private-key end-of-file-fixer flake8 forbid-tabs insert-license lint-dockerfile mixed-line-ending mypy pylint pylint-test setup-order shellcheck].
           You can pass extra arguments including options to to the pre-commit framework as
           <EXTRA_ARGS> passed after --. For example:
 
@@ -590,7 +619,7 @@ 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-apache-license check-executables-have-shebangs check-hooks-apply check-merge-conflict check-xml debug-statements doctoc detect-private-key end-of-file-fixer flake8 forbid-tabs insert-license lint-dockerfile mixed-line-ending mypy pylint pylint-test setup-order shellcheck].
+          [ all all-but-pylint bat-tests check-apache-license check-executables-have-shebangs check-hooks-apply check-merge-conflict check-xml debug-statements doctoc detect-private-key end-of-file-fixer flake8 forbid-tabs insert-license lint-dockerfile mixed-line-ending mypy pylint pylint-test setup-order shellcheck].
           You can pass extra arguments including options to the pre-commit framework as
           <EXTRA_ARGS> passed after --. For example:
 
@@ -646,22 +675,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-kind-cluster
+          Starts kind 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-kind-cluster
+          Recreates kind 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-kind-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-kind-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.
@@ -694,15 +730,19 @@ 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.
 
+  -R, --force-clean-build
+          Force build images without cache at all. This will remove the pulled or build images
+          and start building images from scratch. This might take a long time.
+
+  -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
@@ -715,7 +755,6 @@ This is the current syntax for  `./breeze <./breeze>`_:
 
  .. END BREEZE HELP MARKER
 
-
 Convenience Scripts
 -------------------
 
diff --git a/Dockerfile b/Dockerfile
index 2b0cbb9ae30dd..35f83d38533e5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,12 +15,12 @@
 #
 # WARNING: THIS DOCKERFILE IS NOT INTENDED FOR PRODUCTION USE OR DEPLOYMENT.
 #
-ARG PYTHON_BASE_IMAGE="python:3.6-slim-stretch"
+ARG PYTHON_BASE_IMAGE="python:3.6-slim-buster"
 FROM ${PYTHON_BASE_IMAGE} as main
 
 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,9 +35,9 @@ 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 \
@@ -63,6 +63,7 @@ RUN curl -L https://deb.nodesource.com/setup_10.x | bash - \
            gosu \
            libffi-dev \
            libkrb5-dev \
+           libmariadb-dev-compat \
            libpq-dev \
            libsasl2-2 \
            libsasl2-dev \
@@ -70,7 +71,6 @@ RUN curl -L https://deb.nodesource.com/setup_10.x | bash - \
            libssl-dev \
            locales  \
            netcat \
-           nodejs \
            rsync \
            sasl2-bin \
            sudo \
@@ -79,60 +79,47 @@ RUN curl -L https://deb.nodesource.com/setup_10.x | bash - \
     && 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/
+ENV JAVA_HOME=/usr/lib/jvm/adoptopenjdk-8-hotspot-amd64/
+
+# Setup PIP
+# 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}
+RUN echo "Pip no cache dir: ${PIP_NO_CACHE_DIR}"
+
+# PIP version used to install dependencies
+ARG PIP_VERSION="19.0.2"
+ENV PIP_VERSION=${PIP_VERSION}
+RUN echo "Pip version: ${PIP_VERSION}"
+
+RUN pip install --upgrade pip==${PIP_VERSION}
 
 # 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 software-properties-common \
+    && export APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 \
+    && curl -L https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - \
+    && curl -L 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 \
@@ -197,14 +184,14 @@ RUN curl -L https://download.docker.com/linux/debian/gpg | apt-key add - \
     && apt-get clean && rm -rf /var/lib/apt/lists/*
 
 # Install kubectl
-ARG KUBECTL_VERSION="v1.15.0"
+ARG KUBECTL_VERSION="v1.15.3"
 
 RUN KUBECTL_URL="https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \
   && curl -L "${KUBECTL_URL}" -o "/usr/local/bin/kubectl" \
   && chmod +x /usr/local/bin/kubectl
 
 # Install Kind
-ARG KIND_VERSION="v0.5.0"
+ARG KIND_VERSION="v0.6.1"
 
 RUN KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64" \
    && curl -L "${KIND_URL}" -o "/usr/local/bin/kind" \
@@ -223,19 +210,6 @@ RUN RAT_URL="https://repo1.maven.org/maven2/org/apache/rat/apache-rat/${RAT_VERS
     && jar -tf "${RAT_JAR}" > /dev/null \
     && md5sum -c <<<"$(cat "${RAT_JAR_MD5}") ${RAT_JAR}"
 
-# Setup PIP
-# 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}
-RUN echo "Pip no cache dir: ${PIP_NO_CACHE_DIR}"
-
-# PIP version used to install dependencies
-ARG PIP_VERSION="19.0.2"
-ENV PIP_VERSION=${PIP_VERSION}
-RUN echo "Pip version: ${PIP_VERSION}"
-
-RUN pip install --upgrade pip==${PIP_VERSION}
-
 # Install Google SDK
 ENV GCLOUD_HOME="/opt/gcloud"
 
@@ -281,7 +255,7 @@ RUN mkdir -pv ${AIRFLOW_HOME} \
     mkdir -pv ${AIRFLOW_HOME}/logs
 
 # Increase the value here to force reinstalling Apache Airflow pip dependencies
-ARG PIP_DEPENDENCIES_EPOCH_NUMBER="2"
+ARG PIP_DEPENDENCIES_EPOCH_NUMBER="1"
 ENV PIP_DEPENDENCIES_EPOCH_NUMBER=${PIP_DEPENDENCIES_EPOCH_NUMBER}
 
 # Optimizing installation of Cassandra driver
@@ -305,26 +279,14 @@ 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
+# 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_EXTRAS}]" \
+        && pip uninstall --yes apache-airflow snakebite
 
-# Install NPM dependencies here. The NPM dependencies don't change that often and we already have pip
+# Install node 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/yarn.lock airflow/www/package.json ${AIRFLOW_SOURCES}/airflow/www/
@@ -333,35 +295,28 @@ WORKDIR ${AIRFLOW_SOURCES}/airflow/www
 
 RUN yarn install --frozen-lockfile
 
-WORKDIR ${AIRFLOW_SOURCES}
+# Copy all www files here so that we can run yarn building for production
+COPY airflow/www/ ${AIRFLOW_SOURCES}/airflow/
 
-# 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.
+# Package NPM for production
+RUN yarn run prod
 
 # 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 setup.py setup.cfg ${AIRFLOW_SOURCES}/
+
+COPY airflow/version.py airflow/__init__.py ${AIRFLOW_SOURCES}/airflow/
 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 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}]"
-
-WORKDIR ${AIRFLOW_SOURCES}/airflow/www
-
-# Copy all www files here so that we can run yarn building for production
-COPY airflow/www/ ${AIRFLOW_SOURCES}/airflow/www/
-
-# Package NPM for production
-RUN yarn run prod
+RUN pip install --user -e ".[${AIRFLOW_EXTRAS}]" \
+    && pip uninstall --yes apache-airflow
 
-COPY ./scripts/docker/entrypoint.sh /entrypoint.sh
+COPY scripts/docker/entrypoint.sh /entrypoint.sh
 
 # Copy selected subdirectories only
 COPY .github/ ${AIRFLOW_SOURCES}/.github/
@@ -373,8 +328,7 @@ COPY docs/ ${AIRFLOW_SOURCES}/docs/
 COPY tests/ ${AIRFLOW_SOURCES}/tests/
 COPY airflow/ ${AIRFLOW_SOURCES}/airflow/
 COPY .coveragerc .rat-excludes .flake8 pylintrc LICENSE MANIFEST.in NOTICE CHANGELOG.txt \
-     .github pytest.ini \
-     setup.cfg setup.py \
+     .github pytest.ini Dockerfile \
      ${AIRFLOW_SOURCES}/
 
 # Intall autocomplete
@@ -386,15 +340,15 @@ WORKDIR ${AIRFLOW_SOURCES}
 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}"
+ENV PATH="${HOME}:${PATH}:/root/.local/bin"
 
 EXPOSE 8080
 
-ENTRYPOINT ["/usr/local/bin/dumb-init", "--", "/entrypoint.sh"]
+ENTRYPOINT ["/root/.local/bin/dumb-init", "--", "/entrypoint.sh"]
 
 CMD ["--help"]
diff --git a/TESTING.rst b/TESTING.rst
index 643ceb1d95f67..b9df0f3a47192 100644
--- a/TESTING.rst
+++ b/TESTING.rst
@@ -331,3 +331,54 @@ It will run a backfill job:
 Additionally ``DebugExecutor`` can be used in a fail-fast mode that will make
 all other running or scheduled tasks fail immediately. To enable this option set
 ``AIRFLOW__DEBUG__FAIL_FAST=True`` or adjust ``fail_fast`` option in your ``airflow.cfg``.
+
+
+BASH unit testing (BATS)
+========================
+
+We have started to add tests to cover Bash scripts we have in our codeabase.
+The tests are placed in ``tests\bats`` folder.
+They require BAT CLI to be installed if you want to run them in your
+host or via docker image.
+
+BATS CLI installation
+.....................
+
+You can find installation guide as well as information on how to write
+the bash tests in [BATS installation](https://github.com/bats-core/bats-core#installation)
+
+Running BATS tests in the host
+..............................
+
+Running all tests:
+
+```
+bats -r tests/bats/
+```
+
+Running single test:
+
+```
+bats tests/bats/your_test_file.bats
+```
+
+Running BATS tests via docker
+..............................
+
+Running all tests:
+
+```
+docker run -it --workdir /airflow -v $(pwd):/airflow  bats/bats:latest -r /airflow/tests/bats
+```
+
+Running single test:
+
+```
+docker run -it --workdir /airflow -v $(pwd):/airflow  bats/bats:latest /airflow/tests/bats/your_test_file.bats
+```
+
+BATS usage
+..........
+
+You can read more about using BATS CLI and writing tests in:
+[BATS usage](https://github.com/bats-core/bats-core#usage)
diff --git a/breeze b/breeze
index 46378fde15da7..c45bfcbd6a372 100755
--- a/breeze
+++ b/breeze
@@ -37,20 +37,11 @@ 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"
-}
-
-function read_from_file {
-    cat "${BUILD_CACHE_DIR}/.$1" 2>/dev/null || true
-}
-
-export PYTHON_VERSION="${PYTHON_VERSION:=$(read_from_file PYTHON_VERSION)}"
-
 # shellcheck source=scripts/ci/_utils.sh
 . "${SCRIPTS_CI_DIR}/_utils.sh"
 
+export PYTHON_VERSION="${PYTHON_VERSION:=$(read_from_file PYTHON_VERSION)}"
+
 basic_sanity_checks
 
 script_start
@@ -91,6 +82,9 @@ TEST_TARGET=""
 # Holds docker compose command if the -d flag is used.
 DOCKER_COMPOSE_COMMAND=""
 
+#extra options for Docker compose
+EXTRA_DC_OPTIONS=()
+
 # If true, the docker images are rebuilt locally.
 export AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="false"
 
@@ -98,8 +92,17 @@ export AIRFLOW_CONTAINER_DOCKER_BUILD_NEEDED="false"
 # This can be overridden by -p flag
 export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="false"
 
+# Start airflow-testing image with all the dependencies
+export AIRFLOW_CONTAINER_DEPS="true"
+
+# Do not start Kubernetes cluster by default
+export START_KIND_CLUSTER="false"
+
+# Do not recreate Kubernetes cluster by default
+export RECREATE_KIND_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"}
+export AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE=${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE:="true"}
 
 # By default we do not push images. This can be overridden by -u flag.
 export AIRFLOW_CONTAINER_PUSH_IMAGES=${AIRFLOW_CONTAINER_PUSH_IMAGES:="false"}
@@ -129,6 +132,7 @@ export RUN_DOCKER_COMPOSE="false"
 export RUN_IN_BASH="false"
 export RUN_STATIC_CHECKS="false"
 export RUN_BUILD_DOCS="false"
+export RUN_BUILD="true"
 
 export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD=${AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD:="false"}
 
@@ -183,41 +187,42 @@ function print_badge {
 
 
                                Branch name:        ${AIRFLOW_CONTAINER_BRANCH_NAME}
-                               Docker image:       ${AIRFLOW_CONTAINER_DOCKER_IMAGE}
+                               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_KIND_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_KIND_CLUSTER}
 EOF
         fi
     else
         cat <<EOF
 
    Branch name:        ${AIRFLOW_CONTAINER_BRANCH_NAME}
-   Docker image:       ${AIRFLOW_CONTAINER_DOCKER_IMAGE}
+   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_KIND_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_KIND_CLUSTER}
 EOF
         fi
     fi
@@ -236,14 +241,15 @@ 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 START_KIND_CLUSTER="${START_KIND_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}"
 docker-compose --log-level INFO ${CMD}\$${EXPANSION}"
 EOF
     chmod u+x "${FILE}"
@@ -251,11 +257,9 @@ EOF
 
 # 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 """
 
@@ -363,22 +367,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-kind-cluster
+        Starts kind 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-kind-cluster
+        Recreates kind 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-kind-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-kind-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.
@@ -411,15 +422,19 @@ 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.
 
+-R, --force-clean-build
+        Force build images without cache at all. This will remove the pulled or build images
+        and start building images from scratch. This might take a long time.
+
+-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
@@ -456,39 +471,44 @@ do
       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}";
+    -K|--start-kind-cluster)
+      export START_KIND_CLUSTER="true";
+      echo "Starting kubernetes cluster"
       echo
-      echo "Kubernetes version: ${KUBERNETES_VERSION}"
+      shift ;;
+    -z|--recreate-kind-cluster)
+      export RECREATE_KIND_CLUSTER="true";
+      echo "Recreate kind cluster"
       echo
-      shift 2 ;;
+      shift ;;
     -M|--kubernetes-mode)
       export KUBERNETES_MODE="${2}";
-      echo
       echo "Kubernetes mode: ${KUBERNETES_MODE}"
       echo
       shift 2 ;;
+    -V|--kubernetes-version)
+      export KUBERNETES_VERSION="${2}";
+      echo "Kubernetes version: ${KUBERNETES_VERSION}"
+      echo
+      shift 2 ;;
     -s|--skip-mounting-local-sources)
       SKIP_MOUNTING_LOCAL_SOURCES="true"
       echo "Skip mounting local sources: ${SKIP_MOUNTING_LOCAL_SOURCES}"
       echo
       shift ;;
+    -N|--no-deps)
+      export AIRFLOW_CONTAINER_DEPS="false"
+      echo "Only run airflow-testing image without dependent images"
+      echo
+      shift ;;
     -b|--build-only)
       ENTER_ENVIRONMENT="false"
       export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
@@ -529,22 +549,24 @@ do
       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
+    -R|--force-clean-images)
       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_USE_PULLED_IMAGES_CACHE="false"
       export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
       CLEANUP_IMAGES="true"
       shift ;;
-    -p|--force-pull-images)
+    -L|--use-local-cache)
+      echo "Use local cache to build images"
       echo
+      export AIRFLOW_CONTAINER_USE_NO_CACHE="false"
+      export AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE="false"
+      shift ;;
+    -p|--force-pull-images)
       echo "Force pulling images before build. Uses pulled images as cache."
       echo
       export AIRFLOW_CONTAINER_FORCE_PULL_IMAGES="true"
@@ -569,38 +591,36 @@ do
       export AIRFLOW_CONTAINER_FORCE_DOCKER_BUILD="true"
       shift ;;
     -c|--cleanup-images)
-      echo
       echo "Cleanup the images"
       echo
-      CLEANUP_IMAGES=true
+      export CLEANUP_IMAGES=true
+      export RUN_BUILD="false"
       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 "Initializing local virtualenv"
       echo
       INITIALIZE_LOCAL_VIRTUALENV="true"
       SETUP_AUTOCOMPLETE="false"
       ENTER_ENVIRONMENT=:"false"
+      export RUN_BUILD="false"
       shift ;;
     -a|--setup-autocomplete)
-      echo
-      echo Setting up autocomplete
+      echo "Setting up autocomplete"
       echo
       INITIALIZE_LOCAL_VIRTUALENV="false"
       SETUP_AUTOCOMPLETE="true"
       ENTER_ENVIRONMENT=:"false"
+      export RUN_BUILD="false"
       shift ;;
     -t|--test-target)
       export TEST_TARGET="${2}"
@@ -625,6 +645,8 @@ do
       export RUN_BUILD_DOCS="false"
       export RUN_DOCKER_COMPOSE="true"
       export RUN_STATIC_CHECKS="false"
+      export RUN_BUILD="false"
+      EXTRA_DC_OPTIONS+=("--remove-orphans")
       shift ;;
     -x|--execute-command)
       export RUN_COMMAND="${2}"
@@ -683,10 +705,14 @@ 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 BACKEND=${BACKEND:-${_BREEZE_DEFAULT_BACKEND}}
+
 export KUBERNETES_MODE="${KUBERNETES_MODE:=$(read_from_file KUBERNETES_MODE)}"
+export KUBERNETES_MODE=${KUBERNETES_MODE:=${_BREEZE_DEFAULT_KUBERNETES_MODE}}
+
+export KUBERNETES_VERSION="${KUBERNETES_VERSION:=$(read_from_file KUBERNETES_VERSION)}"
+export KUBERNETES_VERSION=${KUBERNETES_VERSION:=${_BREEZE_DEFAULT_KUBERNETES_VERSION}}
 
 # Here you read DockerHub user/account that you use
 # You can populate your own images in DockerHub this way and work with the,
@@ -700,76 +726,25 @@ export DOCKERHUB_USER="${DOCKERHUB_USER:=${_BREEZE_DEFAULT_DOCKERHUB_USER}}"
 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
+check_for_allowed_params "PYTHON_VERSION"  "Python version" "--python"
+check_for_allowed_params "BACKEND"  "backend" "--backend"
+check_for_allowed_params "KUBERNETES_MODE"  "Kubernetes mode" "--kubernetes-mode"
+check_for_allowed_params "KUBERNETES_VERSION"  "Kubernetes version" "--kubernetes-version"
 
+save_to_file DOCKERHUB_USER
+save_to_file DOCKERHUB_REPO
 
 # 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"
-
-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
+mkdir -pv "${MY_DIR}/.kube"
 
 #################### Cleanup image if requested ########################################
 if [[ "${CLEANUP_IMAGES}" == "true" ]]; then
@@ -902,6 +877,7 @@ 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
+KUBERNETES_DOCKER_COMPOSE_FILE=${SCRIPTS_CI_DIR}/docker-compose-kubernetes.yml
 
 COMPOSE_FILE=${MAIN_DOCKER_COMPOSE_FILE}:${BACKEND_DOCKER_COMPOSE_FILE}
 
@@ -909,23 +885,29 @@ if [[ "${SKIP_MOUNTING_LOCAL_SOURCES}" != "true" ]]; then
     COMPOSE_FILE=${COMPOSE_FILE}:${LOCAL_DOCKER_COMPOSE_FILE}
 fi
 
+if [[ ${START_KIND_CLUSTER} == "true" ]]; then
+    COMPOSE_FILE=${COMPOSE_FILE}:${KUBERNETES_DOCKER_COMPOSE_FILE}
+fi
+
 export COMPOSE_FILE
 
 CI_ENTRYPOINT_FILE="/opt/airflow/scripts/ci/in_container/entrypoint_ci.sh"
 
+export AIRFLOW_CI_IMAGE="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${DEFAULT_BRANCH}-python${PYTHON_VERSION}-ci"
+
 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} "
+DC_RUN_COMMAND_NO_DEPS="run --no-deps --service-ports --rm airflow-testing \"${CI_ENTRYPOINT_FILE} "
 
 LAST_DC_RUN_FILE="cmd_run"
-LAST_DC_RUN_ONLY_AT_FILE="cmd_only_at_run"
+LAST_DC_RUN_NO_DEPS_FILE="cmd_only_at_run"
 LAST_DC_TEST_FILE="test_run"
 LAST_DC_FILE="dc"
 
 # Prepare script for "run command"
 prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}" "${DC_RUN_COMMAND}" "false" '*'
 
-# Prepare script for "run command"
-prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_RUN_ONLY_AT_FILE}" "${DC_RUN_COMMAND_ONLY_AT}" "false" '*'
+# 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" '*'
 
 # Prepare script for "run test"
 prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE}" "${DC_RUN_COMMAND}" "true" '*'
@@ -933,9 +915,15 @@ prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_TEST_FILE}" "${DC_RUN_COMMAND
 # Prepare script for "run docker compose command"
 prepare_command_file "${BUILD_CACHE_DIR}/${LAST_DC_FILE}" '"' "false"
 
-rebuild_ci_image_if_needed
-
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
+if [[ ${RUN_BUILD} == "true" ]] ; then
+    echo
+    echo "Build image if needed"
+    echo
+    rebuild_ci_image_if_needed
+    echo
+    echo
+fi
+export AIRFLOW_CI_IMAGE=\
 ${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
 
 printf '=%.0s' $(seq "${SEPARATOR_WIDTH}")
@@ -966,7 +954,7 @@ if [[ ! -f ${SUPPRESS_CHEATSHEET_FILE} ]]; then
     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} "\
+    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]"
@@ -1029,11 +1017,17 @@ 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}" "$@"
+        set +u
+        "${BUILD_CACHE_DIR}/${LAST_DC_FILE}" "${DOCKER_COMPOSE_COMMAND}" "${EXTRA_DC_OPTIONS[@]}" "$@"
+        set -u
     elif [[ ${RUN_IN_BASH} == "true" ]]; then
         "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}" "${RUN_COMMAND}" "$@"
     else
-        "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}"
+        if [[ ${AIRFLOW_CONTAINER_DEPS} == "true" ]]; then
+                 "${BUILD_CACHE_DIR}/${LAST_DC_RUN_FILE}"
+        else
+             "${BUILD_CACHE_DIR}/${LAST_DC_RUN_NO_DEPS_FILE}"
+        fi
     fi
 else
     if [[ ${RUN_STATIC_CHECKS} == "true" ]]; then
diff --git a/breeze-complete b/breeze-complete
index 867542b532377..63308aefa01b4 100644
--- a/breeze-complete
+++ b/breeze-complete
@@ -18,28 +18,29 @@
 # 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-apache-license check-executables-have-shebangs check-hooks-apply check-merge-conflict check-xml debug-statements doctoc detect-private-key end-of-file-fixer flake8 forbid-tabs insert-license lint-dockerfile mixed-line-ending mypy pylint pylint-test setup-order shellcheck"
+_BREEZE_ALLOWED_KUBERNETES_VERSIONS=" v1.15.3 v1.16.2 "
+_BREEZE_ALLOWED_STATIC_CHECKS=" all all-but-pylint bat-tests check-apache-license check-executables-have-shebangs check-hooks-apply check-merge-conflict check-xml debug-statements doctoc detect-private-key end-of-file-fixer flake8 forbid-tabs insert-license lint-dockerfile mixed-line-ending mypy pylint pylint-test setup-order shellcheck"
 _BREEZE_DEFAULT_DOCKERHUB_USER="apache"
 _BREEZE_DEFAULT_DOCKERHUB_REPO="airflow"
 
 _BREEZE_SHORT_OPTIONS="
-h P: E: B: K: M:
-s b O
+h P: B: K Z
+M: V: N
+s b O N
 v y n C A
-r R p u
+r p R L u
 c D: H: e a
 t: d: k x: S: F:
 "
 
 _BREEZE_LONG_OPTIONS="
-help python: env: backend: kubernetes-version: kubernetes-mode:
+help python: backend: start-kind-cluster recreate-kind-cluster
+kubernetes-mode: kubernetes-version: no-deps
 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
+force-build-images force-pull-images force-clean-images use-local-cache 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:
 "
@@ -52,14 +53,12 @@ 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} ;;
         -S | --static-check )
             _BREEZE_KNOWN_VALUES=${_BREEZE_ALLOWED_STATIC_CHECKS} ;;
         -F | --static-check-all-files )
diff --git a/hooks/build b/hooks/build
index 5923b545c709d..cf5ae81aada4c 100755
--- a/hooks/build
+++ b/hooks/build
@@ -153,7 +153,7 @@ if [[ "${AIRFLOW_CONTAINER_USE_NO_CACHE:=}" == "true" ]]; then
 fi
 
 
-# You can set AIRFLOW_CONTAINER_USE_DOCKER_CACHE to false if you do not want to use pulled images
+# You can set AIRFLOW_CONTAINER_USE_PULLED_IMAGES_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"}
@@ -176,7 +176,7 @@ else
 fi
 
 # Extras used to build cache and CI image
-AIRFLOW_CI_EXTRAS=${AIRFLOW_CI_EXTRAS:="devel_ci"}
+AIRFLOW_EXTRAS=${AIRFLOW_EXTRAS:="devel_ci"}
 
 # Whether this is a release build
 AIRFLOW_RELEASE_BUILD=${AIRFLOW_RELEASE_BUILD:="false"}
@@ -253,7 +253,7 @@ fi
 
 start_step "Populating cache"
 
-DOCKER_CACHE_DIRECTIVE_CI=()
+DOCKER_CACHE_DIRECTIVE=()
 
 if [[ "${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE}" == "true" ]]; then
     echo
@@ -281,7 +281,7 @@ if [[ "${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE}" == "true" ]]; then
         IMAGES_TO_PULL="${IMAGES_TO_PULL} ${AIRFLOW_CI_IMAGE}"
     fi
 
-    DOCKER_CACHE_DIRECTIVE_CI=()
+    DOCKER_CACHE_DIRECTIVE=()
     if [[ ${IMAGES_TO_PULL} == "" ]]; then
         echo
         echo "Skipping building of all images."
@@ -320,7 +320,7 @@ if [[ "${AIRFLOW_CONTAINER_USE_PULLED_IMAGES_CACHE}" == "true" ]]; then
                 echo
             fi
             if [[ "${IMAGE}" == "${AIRFLOW_CI_IMAGE}" ]]; then
-                DOCKER_CACHE_DIRECTIVE_CI+=("--cache-from" "${IMAGE}")
+                DOCKER_CACHE_DIRECTIVE+=("--cache-from" "${IMAGE}")
             else
                 echo
                 echo "Don't know how to set cache directive for ${IMAGE}. Exiting"
@@ -334,29 +334,29 @@ fi
 start_step "Setting cache options"
 
 if [[ "${AIRFLOW_CONTAINER_USE_NO_CACHE}" == "true" ]]; then
-    DOCKER_CACHE_DIRECTIVE_CI+=("--no-cache")
+    DOCKER_CACHE_DIRECTIVE+=("--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 "CI build: ${DOCKER_CACHE_DIRECTIVE[*]}"
     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[*]}"
+    echo "CI build: ${DOCKER_CACHE_DIRECTIVE[*]}"
     set -u
     echo
 else
-    DOCKER_CACHE_DIRECTIVE_CI=()
+    DOCKER_CACHE_DIRECTIVE=()
     echo
     echo "Use default cache from locally built images."
     echo
     echo "Cache directives used: "
     set +u
-    echo "CI build: ${DOCKER_CACHE_DIRECTIVE_CI[*]}"
+    echo "CI build: ${DOCKER_CACHE_DIRECTIVE[*]}"
     set -u
     echo
 fi
diff --git a/scripts/ci/_utils.sh b/scripts/ci/_utils.sh
index 5f96584195f2c..ea41f3c2fac28 100644
--- a/scripts/ci/_utils.sh
+++ b/scripts/ci/_utils.sh
@@ -60,6 +60,8 @@ export AIRFLOW_CONTAINER_PUSH_IMAGES=${AIRFLOW_CONTAINER_PUSH_IMAGES:="false"}
 # 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_KIND_CLUSTER=${START_KIND_CLUSTER:="false"}
 #
 # Sets mounting of host volumes to container for static checks
 # unless AIRFLOW_MOUNT_HOST_VOLUMES_FOR_STATIC_CHECKS is not true
@@ -175,7 +177,7 @@ function remove_cache_directory() {
 function check_file_md5sum {
     local FILE="${1}"
     local MD5SUM
-    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${THE_IMAGE_TYPE}"
+    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${PYTHON_VERSION}/${THE_IMAGE_TYPE}"
     mkdir -pv "${MD5SUM_CACHE_DIR}"
     MD5SUM=$(md5sum "${FILE}")
     local MD5SUM_FILE
@@ -205,7 +207,7 @@ function check_file_md5sum {
 function move_file_md5sum {
     local FILE="${1}"
     local MD5SUM_FILE
-    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${THE_IMAGE_TYPE}"
+    local MD5SUM_CACHE_DIR="${BUILD_CACHE_DIR}/${DEFAULT_BRANCH}/${PYTHON_VERSION}/${THE_IMAGE_TYPE}"
     mkdir -pv "${MD5SUM_CACHE_DIR}"
     MD5SUM_FILE="${MD5SUM_CACHE_DIR}"/$(basename "${FILE}").md5sum
     local MD5SUM_FILE_NEW
@@ -513,9 +515,9 @@ function rebuild_ci_image_if_needed() {
 
     export THE_IMAGE_TYPE="CI"
 
-    rebuild_image_if_needed
-
     export AIRFLOW_CI_IMAGE="${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${DEFAULT_BRANCH}-python${PYTHON_VERSION}-ci"
+
+    rebuild_image_if_needed
 }
 
 
@@ -601,7 +603,7 @@ function run_flake8() {
 
     if [[ "${#FILES[@]}" == "0" ]]; then
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -613,7 +615,7 @@ function run_flake8() {
             | tee -a "${OUTPUT_LOG}"
     else
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -626,9 +628,21 @@ function run_flake8() {
     fi
 }
 
+function run_bats_tests() {
+    FILES=("$@")
+    if [[ "${#FILES[@]}" == "0" ]]; then
+        docker run --workdir /airflow -v "$(pwd):/airflow"  \
+            bats/bats:latest --tap -r /airflow/tests/bats | tee -a "${OUTPUT_LOG}"
+    else
+        docker run --workdir /airflow -v "$(pwd):/airflow" \
+            bats/bats:latest --tap -r "${FILES[@]}" | tee -a "${OUTPUT_LOG}"
+    fi
+}
+
+
 function run_docs() {
     docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" -t \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -642,7 +656,7 @@ function run_docs() {
 
 function run_check_license() {
     docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" -t \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -658,7 +672,7 @@ function run_mypy() {
     FILES=("$@")
     if [[ "${#FILES[@]}" == "0" ]]; then
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -670,7 +684,7 @@ function run_mypy() {
             | tee -a "${OUTPUT_LOG}"
     else
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init" \
+            --entrypoint "/root/.local/bin/dumb-init" \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -687,7 +701,7 @@ function run_pylint_main() {
     FILES=("$@")
     if [[ "${#FILES[@]}" == "0" ]]; then
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -699,7 +713,7 @@ function run_pylint_main() {
             | tee -a "${OUTPUT_LOG}"
     else
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init" \
+            --entrypoint "/root/.local/bin/dumb-init" \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -717,7 +731,7 @@ function run_pylint_tests() {
     FILES=("$@")
     if [[ "${#FILES[@]}" == "0" ]]; then
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -729,7 +743,7 @@ function run_pylint_tests() {
             | tee -a "${OUTPUT_LOG}"
     else
         docker run "${AIRFLOW_CONTAINER_EXTRA_DOCKER_FLAGS[@]}" \
-            --entrypoint "/usr/local/bin/dumb-init"  \
+            --entrypoint "/root/.local/bin/dumb-init"  \
             --env PYTHONDONTWRITEBYTECODE \
             --env AIRFLOW_CI_VERBOSE="${VERBOSE}" \
             --env AIRFLOW_CI_SILENT \
@@ -889,14 +903,16 @@ function build_image_on_ci() {
 
     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'
+            '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'
+        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
@@ -930,3 +946,38 @@ function build_image_on_ci() {
     unset AIRFLOW_CONTAINER_FORCE_PULL_IMAGES
     unset FORCE_BUILD
 }
+
+function read_from_file {
+    cat "${BUILD_CACHE_DIR}/.$1" 2>/dev/null || true
+}
+
+function save_to_file {
+    # shellcheck disable=SC2005
+    echo "$(eval echo "\$$1")" > "${BUILD_CACHE_DIR}/.$1"
+}
+
+function check_for_allowed_params {
+    _VARIABLE_NAME="${1}"
+    _VARIABLE_DESCRIPTIVE_NAME="${2}"
+    _FLAG="${3}"
+    _ALLOWED_VALUES_ENV_NAME="_BREEZE_ALLOWED_${_VARIABLE_NAME}S"
+    _ALLOWED_VALUES=${!_ALLOWED_VALUES_ENV_NAME}
+    _VALUE=${!_VARIABLE_NAME}
+    if [[ ${_ALLOWED_VALUES:=} != *" ${_VALUE} "* ]]; then
+        echo >&2
+        echo >&2 "ERROR:  Allowed ${_VARIABLE_DESCRIPTIVE_NAME}: [${_ALLOWED_VALUES}]. Is: '${!_VARIABLE_NAME}'."
+        echo >&2
+        echo >&2 "Switch to supported Kubernetes version with ${_FLAG} flag."
+
+        if [[ -n ${!_VARIABLE_NAME} && \
+            -f "${BUILD_CACHE_DIR}/.${_VARIABLE_NAME}" && \
+            ${!_VARIABLE_NAME} == $(cat "${BUILD_CACHE_DIR}/.${_VARIABLE_NAME}" ) ]]; then
+            echo >&2
+            echo >&2 "Removing ${BUILD_CACHE_DIR}/.${_VARIABLE_NAME}. Next time you run it, it should be OK."
+            echo >&2
+            rm -f "${BUILD_CACHE_DIR}/.${_VARIABLE_NAME}"
+        fi
+        exit 1
+    fi
+    save_to_file "${_VARIABLE_NAME}"
+}
diff --git a/scripts/ci/ci_isort.sh b/scripts/ci/ci_bat_tests.sh
similarity index 88%
rename from scripts/ci/ci_isort.sh
rename to scripts/ci/ci_bat_tests.sh
index 01e7b1163da31..fe2bc3a27632a 100755
--- a/scripts/ci/ci_isort.sh
+++ b/scripts/ci/ci_bat_tests.sh
@@ -21,19 +21,16 @@ 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"}
+
+export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
 
 # shellcheck source=scripts/ci/_utils.sh
 . "${MY_DIR}/_utils.sh"
 
 basic_sanity_checks
 
-force_python_3_5
-
 script_start
 
-rebuild_ci_image_if_needed
-
-run_flake8 "$@"
+run_bats_tests "$@"
 
 script_end
diff --git a/scripts/ci/ci_before_install.sh b/scripts/ci/ci_before_install.sh
index 2386406415745..daccfe6fe735b 100755
--- a/scripts/ci/ci_before_install.sh
+++ b/scripts/ci/ci_before_install.sh
@@ -29,7 +29,7 @@ script_start
 
 build_image_on_ci
 
-KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
+KUBERNETES_MODE=${KUBERNETES_MODE:=""}
 
 mkdir -p "${AIRFLOW_SOURCES}/files"
 
diff --git a/scripts/ci/ci_run_airflow_testing.sh b/scripts/ci/ci_run_airflow_testing.sh
index d86086806b7b0..571d3a8c3da9e 100755
--- a/scripts/ci/ci_run_airflow_testing.sh
+++ b/scripts/ci/ci_run_airflow_testing.sh
@@ -40,8 +40,6 @@ 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"}
@@ -58,11 +56,8 @@ 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 "Using docker image: ${AIRFLOW_CI_IMAGE} for docker compose runs"
 echo
 
 HOST_USER_ID="$(id -ur)"
@@ -71,36 +66,36 @@ 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_KIND_CLUSTER} == "true" ]]; then
+    export KUBERNETES_MODE=${KUBERNETES_MODE:="git_mode"}
+    export KUBERNETES_VERSION=${KUBERNETES_VERSION:="v1.15.3"}
+
+    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/docker-compose-kubernetes.yml b/scripts/ci/docker-compose-kubernetes.yml
new file mode 100644
index 0000000000000..e751ca46d2986
--- /dev/null
+++ b/scripts/ci/docker-compose-kubernetes.yml
@@ -0,0 +1,33 @@
+# 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.
+---
+version: "2.2"
+services:
+  docker:
+    image: docker:19.03.2-dind
+    privileged: true
+    environment:
+      - DOCKER_TLS_CERTDIR=
+      - DOCKER_TLS_VERIFY=
+  airflow-testing:
+    environment:
+      - KUBERNETES_MODE
+      - KUBERNETES_VERSION
+      - RECREATE_KIND_CLUSTER
+      - DOCKER_HOST=tcp://docker:2375
+    depends_on:
+      - docker
diff --git a/scripts/ci/docker-compose-local.yml b/scripts/ci/docker-compose-local.yml
index e44fab57e4ff2..c9a0965d39380 100644
--- a/scripts/ci/docker-compose-local.yml
+++ b/scripts/ci/docker-compose-local.yml
@@ -30,31 +30,35 @@ services:
     # or those that might be useful to see in the host as output of the
     # tests (such as logs)
     volumes:
+      - ../../.bash_aliases:/root/.bash_aliases:cached
+      - ../../.bash_history:/root/.bash_history:cached
+      - ../../.coveragerc:/opt/airflow/.coveragerc:cached
+      - ../../.dockerignore:/opt/airflow/.dockerignore:cached
+      - ../../.flake8:/opt/airflow/.flake8:cached
+      - ../../.github:/opt/airflow/.github:cached
+      - ../../.inputrc:/root/.inputrc:cached
+      - ../../.kube:/root/.kube:cached
+      - ../../.rat-excludes:/opt/airflow/.rat-excludes:cached
+      - ../../CHANGELOG.txt:/opt/airflow/CHANGELOG:cached
+      - ../../Dockerfile:/opt/airflow/Dockerfile:cached
+      - ../../LICENSE:/opt/airflow/LICENSE:cached
+      - ../../MANIFEST.in:/opt/airflow/MANIFEST.in:cached
+      - ../../NOTICE:/opt/airflow/NOTICE:cached
       - ../../airflow:/opt/airflow/airflow:cached
-      - ../../setup.cfg:/opt/airflow/setup.cfg:cached
-      - ../../setup.py:/opt/airflow/setup.py:cached
+      - ../../common:/opt/airflow/common:cached
       - ../../dags:/opt/airflow/dags:cached
       - ../../dev:/opt/airflow/dev:cached
       - ../../docs:/opt/airflow/docs:cached
+      - ../../files:/files:cached
       - ../../hooks:/opt/airflow/hooks:cached
-      - ../../scripts:/opt/airflow/scripts:cached
-      - ../../tests:/opt/airflow/tests:cached
-      - ../../.coveragerc:/opt/airflow/.coveragerc:cached
-      - ../../LICENSE:/opt/airflow/LICENSE:cached
-      - ../../MANIFEST.in:/opt/airflow/MANIFEST.in:cached
-      - ../../NOTICE:/opt/airflow/NOTICE:cached
-      - ../../CHANGELOG.txt:/opt/airflow/CHANGELOG:cached
-      - ../../.github:/opt/airflow/.github:cached
-      - ../../.bash_history:/root/.bash_history:cached
-      - ../../.bash_aliases:/root/.bash_aliases:cached
-      - ../../.inputrc:/root/.inputrc:cached
-      - ../../.flake8:/opt/airflow/.flake8:cached
+      - ../../logs:/root/airflow/logs:cached
       - ../../pylintrc:/opt/airflow/pylintrc:cached
       - ../../pytest.ini:/opt/airflow/pytest.ini:cached
-      - ../../.rat-excludes:/opt/airflow/.rat-excludes:cached
-      - ../../logs:/root/airflow/logs:cached
+      - ../../scripts:/opt/airflow/scripts:cached
+      - ../../setup.cfg:/opt/airflow/setup.cfg:cached
+      - ../../setup.py:/opt/airflow/setup.py:cached
+      - ../../tests:/opt/airflow/tests:cached
       - ../../tmp:/opt/airflow/tmp:cached
-      - ../../files:/files:cached
     environment:
       - HOST_USER_ID
       - HOST_GROUP_ID
diff --git a/scripts/ci/docker-compose.yml b/scripts/ci/docker-compose.yml
index 9aef686d9f2ca..3dc866214117c 100644
--- a/scripts/ci/docker-compose.yml
+++ b/scripts/ci/docker-compose.yml
@@ -61,15 +61,8 @@ services:
     hostname: krb5-kdc-server
     domainname: example.com
 
-  docker:
-    image: docker:19.03.2-dind
-    privileged: true
-    environment:
-      - DOCKER_TLS_CERTDIR=
-      - DOCKER_TLS_VERIFY=
-
   airflow-testing:
-    image: ${AIRFLOW_CONTAINER_DOCKER_IMAGE}
+    image: ${AIRFLOW_CI_IMAGE}
     init: true
     entrypoint: ["/bin/bash", "-c"]
     environment:
@@ -83,8 +76,6 @@ services:
       - KRB5_CONFIG=/etc/krb5.conf
       - 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 +92,8 @@ services:
       - RUN_TESTS
       - AIRFLOW_CI_VERBOSE
       - AIRFLOW_CI_SILENT
-      - AIRFLOW_CONTAINER_DOCKER_IMAGE
-      - KUBERNETES_VERSION
-      - KUBERNETES_MODE
+      - AIRFLOW_CI_IMAGE
+      - START_KIND_CLUSTER
       - BREEZE
     depends_on:
       - postgres
@@ -114,4 +104,3 @@ services:
       - redis
       - openldap
       - krb5-kdc-server
-      - docker
diff --git a/scripts/ci/in_container/entrypoint_ci.sh b/scripts/ci/in_container/entrypoint_ci.sh
index 4531cc7346cfa..82b0ac129ba05 100755
--- a/scripts/ci/in_container/entrypoint_ci.sh
+++ b/scripts/ci/in_container/entrypoint_ci.sh
@@ -37,11 +37,12 @@ TRAVIS=${TRAVIS:=}
 
 AIRFLOW_SOURCES=$(cd "${MY_DIR}/../../.." || exit 1; pwd)
 
-PYTHON_VERSION=${PYTHON_VERSION:=3.6}
-ENV=${ENV:=docker}
+PYTHON_VERSION=${PYTHON_VERSION:=3.5}
 BACKEND=${BACKEND:=sqlite}
-KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
 KUBERNETES_MODE=${KUBERNETES_MODE:=""}
+KUBERNETES_VERSION=${KUBERNETES_VERSION:=""}
+RECREATE_KIND_CLUSTER=${RECREATE_KIND_CLUSTER:="true"}
+START_KIND_CLUSTER=${START_KIND_CLUSTER:="false"}
 
 export AIRFLOW_HOME=${AIRFLOW_HOME:=${HOME}}
 
@@ -115,13 +116,13 @@ sudo rm -rf "${AIRFLOW_SOURCES}"/tmp/*
 mkdir -p "${AIRFLOW_SOURCES}"/logs/
 mkdir -p "${AIRFLOW_SOURCES}"/tmp/
 
-if [[ "${ENV}" == "docker" ]]; then
+if [[ "${START_KIND_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
@@ -131,52 +132,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
@@ -186,6 +189,12 @@ mkdir -pv "${AIRFLOW_HOME}/logs/"
 
 cp -f "${MY_DIR}/airflow_ci.cfg" "${AIRFLOW_HOME}/unittests.cfg"
 
+if [[ "${START_KIND_CLUSTER}" == "true" ]]; then
+    export AIRFLOW_CI_KUBERNETES_IMAGE=${AIRFLOW_CI_IMAGE}-kubernetes
+    "${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
@@ -223,6 +232,7 @@ if [[ -z "${KUBERNETES_VERSION}" ]]; then
 else
     export SKIP_INIT_DB=true
     echo "Set up Kubernetes cluster for tests"
+    export AIRFLOW_CI_KUBERNETES_IMAGE=${AIRFLOW_CI_IMAGE}-kubernetes
     "${MY_DIR}/../kubernetes/setup_kubernetes.sh"
     "${MY_DIR}/../kubernetes/app/deploy_app.sh" -d "${KUBERNETES_MODE}"
 
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..7ecb3e2297b03
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/app/deploy_app.sh
@@ -0,0 +1,182 @@
+#!/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
+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=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}}
+
+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/rebuild_airflow_image.sh b/scripts/ci/in_container/kubernetes/docker/rebuild_airflow_image.sh
new file mode 100755
index 0000000000000..327cd40428286
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/docker/rebuild_airflow_image.sh
@@ -0,0 +1,87 @@
+#!/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}"
+
+if [[ "$(docker images -q "${AIRFLOW_CI_IMAGE}" 2> /dev/null)" == "" ]]; then
+    echo
+    echo "Pulling base kubernetes image from ${AIRFLOW_CI_IMAGE}"
+    echo
+    docker pull "${AIRFLOW_CI_IMAGE}"
+else
+    echo
+    echo "Not pulling base kubernetes image - it's already pulled"
+    echo
+fi
+
+# Copy entrypoing so that it is in the same place in Docker as in souurces
+cp "/entrypoint.sh" "${AIRFLOW_SOURCES}/entrypoint.sh"
+
+echo
+echo "(Re)building kubernetes image from ${AIRFLOW_CI_IMAGE} with latest sources"
+echo
+docker build \
+    --cache-from "${AIRFLOW_CI_IMAGE}" \
+    --tag="${AIRFLOW_CI_IMAGE}" \
+    --target="main" \
+    -f Dockerfile .
+echo
+echo "Adding kubernetes-specific scripts. Building ${AIRFLOW_CI_KUBERNETES_IMAGE} from ${AIRFLOW_CI_IMAGE}"
+echo
+docker build \
+    --build-arg AIRFLOW_CI_IMAGE="${AIRFLOW_CI_IMAGE}" \
+    --cache-from "${AIRFLOW_CI_IMAGE}" \
+    --tag="${AIRFLOW_CI_KUBERNETES_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_KUBERNETES_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..f1eaa8605fa88
--- /dev/null
+++ b/scripts/ci/in_container/kubernetes/setup_kubernetes.sh
@@ -0,0 +1,101 @@
+#!/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_KIND_CLUSTER=${RECREATE_KIND_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
+    rm -rf "${HOME}/.kube/*"
+}
+
+ALL_CLUSTERS=$(kind get clusters || true)
+if [[ ${ALL_CLUSTERS} == *"${CLUSTER_NAME}"* ]]; then
+    echo
+    echo "Cluster ${CLUSTER_NAME} is already created"
+    echo
+    if [[ ${RECREATE_KIND_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
+
+kubectl config set "clusters.kind-${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/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_extract_tests.sh b/scripts/ci/in_container/run_extract_tests.sh
deleted file mode 100755
index 75bef099cc048..0000000000000
--- a/scripts/ci/in_container/run_extract_tests.sh
+++ /dev/null
@@ -1,38 +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.
-
-#
-# Bash sanity settings (error on exit, complain for undefined vars, error when pipe fails)
-set -euo pipefail
-
-MY_DIR=$(cd "$(dirname "$0")" || exit 1; pwd)
-
-# shellcheck source=scripts/ci/in_container/_in_container_utils.sh
-. "${MY_DIR}/_in_container_utils.sh"
-
-in_container_basic_sanity_check
-
-in_container_script_start
-
-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"
-
-in_container_script_end
diff --git a/scripts/ci/in_container/run_pylint_main.sh b/scripts/ci/in_container/run_pylint_main.sh
index 3096e9aa521fd..e967db757c844 100755
--- a/scripts/ci/in_container/run_pylint_main.sh
+++ b/scripts/ci/in_container/run_pylint_main.sh
@@ -58,7 +58,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/local_ci_enter_environment.sh b/scripts/ci/local_ci_enter_environment.sh
index d6d524940ab78..0774410acd6e2 100755
--- a/scripts/ci/local_ci_enter_environment.sh
+++ b/scripts/ci/local_ci_enter_environment.sh
@@ -21,7 +21,7 @@ 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 PYTHON_VERSION=${PYTHON_VERSION:="3.5"}
 export VERBOSE=${VERBOSE:="false"}
 
 echo
diff --git a/scripts/ci/local_ci_fix_ownership.sh b/scripts/ci/local_ci_fix_ownership.sh
index 0f6e6027738ca..7144462bd7ade 100755
--- a/scripts/ci/local_ci_fix_ownership.sh
+++ b/scripts/ci/local_ci_fix_ownership.sh
@@ -32,7 +32,7 @@ script_start
 
 export PYTHON_VERSION=${PYTHON_VERSION:-3.6}
 
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
+export AIRFLOW_CI_IMAGE=\
 ${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
 
 HOST_USER_ID="$(id -ur)"
diff --git a/scripts/ci/local_ci_stop_environment.sh b/scripts/ci/local_ci_stop_environment.sh
index 823cd0d507c99..41dc510783327 100755
--- a/scripts/ci/local_ci_stop_environment.sh
+++ b/scripts/ci/local_ci_stop_environment.sh
@@ -29,12 +29,12 @@ basic_sanity_checks
 
 script_start
 
-export PYTHON_VERSION=${PYTHON_VERSION:="3.6"}
+export PYTHON_VERSION=${PYTHON_VERSION:="3.5"}
 
 # Default branch name for triggered builds is master
 export AIRFLOW_CONTAINER_BRANCH_NAME=${AIRFLOW_CONTAINER_BRANCH_NAME:="master"}
 
-export AIRFLOW_CONTAINER_DOCKER_IMAGE=\
+export AIRFLOW_CI_IMAGE=\
 ${DOCKERHUB_USER}/${DOCKERHUB_REPO}:${AIRFLOW_CONTAINER_BRANCH_NAME}-python${PYTHON_VERSION}-ci
 
 HOST_USER_ID="$(id -ur)"
diff --git a/scripts/ci/kubernetes/docker/build.sh b/scripts/ci/pre_commit_bat_tests.sh
similarity index 67%
rename from scripts/ci/kubernetes/docker/build.sh
rename to scripts/ci/pre_commit_bat_tests.sh
index 8f11bb1aadd1f..89ce562386ca5 100755
--- a/scripts/ci/kubernetes/docker/build.sh
+++ b/scripts/ci/pre_commit_bat_tests.sh
@@ -16,16 +16,10 @@
 # specific language governing permissions and limitations
 # under the License.
 
-IMAGE=${IMAGE:-airflow}
-TAG=${TAG:-latest}
-DIRNAME=$(cd "$(dirname "$0")" && pwd)
-AIRFLOW_ROOT="${DIRNAME}/../../../.."
+set -uo pipefail
 
-set -e
+MY_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+export FORCE_ANSWER_TO_QUESTIONS=${FORCE_ANSWER_TO_QUESTIONS:="quit"}
+export REMEMBER_LAST_ANSWER="true"
 
-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}"
+"${MY_DIR}/ci_bat_tests.sh" "${@}"
diff --git a/scripts/ci/kubernetes/setup_kubernetes.sh b/tests/bats/bats_utils.bash
old mode 100755
new mode 100644
similarity index 59%
rename from scripts/ci/kubernetes/setup_kubernetes.sh
rename to tests/bats/bats_utils.bash
index d24d53a4bcbda..bf89f649b487f
--- a/scripts/ci/kubernetes/setup_kubernetes.sh
+++ b/tests/bats/bats_utils.bash
@@ -1,4 +1,3 @@
-#!/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
@@ -16,22 +15,6 @@
 # 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!"
+AIRFLOW_SOURCES=$(pwd)
+export AIRFLOW_SOURCES
+source scripts/ci/_utils.sh
diff --git a/tests/bats/test_breeze_params.bats b/tests/bats/test_breeze_params.bats
new file mode 100644
index 0000000000000..45adb8292c548
--- /dev/null
+++ b/tests/bats/test_breeze_params.bats
@@ -0,0 +1,110 @@
+#!/usr/bin/env bats
+# shellcheck disable=SC2030
+# shellcheck disable=SC2031
+# 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.
+
+
+setup() {
+  load bats_utils
+  rm -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM"
+}
+
+teardown() {
+  load bats_utils
+  rm -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM"
+}
+
+@test "Test missing value for a parameter" {
+  load bats_utils
+  export _BREEZE_ALLOWED_TEST_PARAMS=" a b c "
+  run check_for_allowed_params "TEST_PARAM"  "Test Param" "--message"
+  diff <(echo "${output}") - <<EOF
+
+ERROR:  Allowed Test Param: [ a b c ]. Is: ''.
+
+Switch to supported Kubernetes version with --message flag.
+EOF
+  [ "${status}" == "1" ]
+}
+
+@test "Test wrong value for a parameter but proper stored in the .build/PARAM" {
+  load bats_utils
+
+  export _BREEZE_ALLOWED_TEST_PARAMS=" a b c "
+  export TEST_PARAM=x
+  echo "a" > "${AIRFLOW_SOURCES}/.build/.TEST_PARAM"
+  run check_for_allowed_params "TEST_PARAM"  "Test Param" "--message"
+  diff <(echo "${output}") - <<EOF
+
+ERROR:  Allowed Test Param: [ a b c ]. Is: 'x'.
+
+Switch to supported Kubernetes version with --message flag.
+EOF
+  [ -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM" ]
+  diff <(cat "${AIRFLOW_SOURCES}/.build/.TEST_PARAM") <(echo "a")
+  [ "${status}" == "1" ]
+}
+
+@test "Test wrong value for a parameter stored in the .build/PARAM" {
+  load bats_utils
+
+  export _BREEZE_ALLOWED_TEST_PARAMS=" a b c "
+  export TEST_PARAM=x
+  echo "x" > "${AIRFLOW_SOURCES}/.build/.TEST_PARAM"
+  run check_for_allowed_params "TEST_PARAM"  "Test Param" "--message"
+  diff <(echo "${output}") - <<EOF
+
+ERROR:  Allowed Test Param: [ a b c ]. Is: 'x'.
+
+Switch to supported Kubernetes version with --message flag.
+
+Removing ${AIRFLOW_SOURCES}/.build/.TEST_PARAM. Next time you run it, it should be OK.
+EOF
+  [ ! -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM" ]
+  [ "${status}" == "1" ]
+}
+
+
+@test "Test correct value for a parameter" {
+  load bats_utils
+  export _BREEZE_ALLOWED_TEST_PARAMS=" a b c "
+  export TEST_PARAM=a
+  run check_for_allowed_params "TEST_PARAM"  "Test Param" "--message"
+  diff <(echo "${output}") <(echo "")
+  [ -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM" ]
+  diff <(echo "a") <(cat "${AIRFLOW_SOURCES}/.build/.TEST_PARAM")
+  [ "${status}" == "0" ]
+}
+
+@test "Test read_parameter from missing file" {
+  load bats_utils
+  run read_from_file TEST_PARAM
+  [ -z "${TEST_FILE}" ]
+  diff <(echo "${output}") <(echo "")
+  [ ! -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM" ]
+  [ "${status}" == "0" ]
+}
+
+@test "Test read_parameter from file" {
+  load bats_utils
+  echo "a" > "${AIRFLOW_SOURCES}/.build/.TEST_PARAM"
+  run read_from_file TEST_PARAM
+  diff <(echo "${output}") <(echo "a")
+  [ -f "${AIRFLOW_SOURCES}/.build/.TEST_PARAM" ]
+  [ "${status}" == "0" ]
+}
diff --git a/scripts/ci/kubernetes/docker/Dockerfile b/tests/bats/test_empty_test.bats
similarity index 80%
rename from scripts/ci/kubernetes/docker/Dockerfile
rename to tests/bats/test_empty_test.bats
index 6ccb44358765f..a9c0fd4f72ddc 100644
--- a/scripts/ci/kubernetes/docker/Dockerfile
+++ b/tests/bats/test_empty_test.bats
@@ -1,3 +1,5 @@
+#!/usr/bin/env bats
+
 # 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
@@ -14,12 +16,9 @@
 # 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"]
+@test "empty test" {
+  load bats_utils
+  run pwd
+  [ "${status}" == 0 ]
+}
diff --git a/tests/integration/kubernetes/test_kubernetes_executor.py b/tests/integration/kubernetes/test_kubernetes_executor.py
index 14fb2958f348e..df158fc7ab460 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') and os.environ.get('ENV', 'kubernetes') == 'kubernetes':
+    if os.environ.get('START_KIND_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 de42b6e9d70e2..b2adf5ca5669c 100644
--- a/tests/integration/kubernetes/test_kubernetes_pod_operator.py
+++ b/tests/integration/kubernetes/test_kubernetes_pod_operator.py
@@ -40,7 +40,7 @@
 try:
     check_call(["/usr/local/bin/kubectl", "get", "pods"])
 except Exception as e:  # pylint: disable=broad-except
-    if os.environ.get('KUBERNETES_VERSION') and os.environ.get('ENV', 'kubernetes') == 'kubernetes':
+    if os.environ.get('START_KIND_CLUSTER') == "true":
         raise e
     else:
         raise unittest.SkipTest(
diff --git a/tests/test_core.py b/tests/test_core.py
index 165c9012f5aac..0e33650859aeb 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -76,7 +76,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_KIND_CLUSTER') == "true":
             return
 
         session = Session()
