From a2aa160cea36fd8969e38eafb352154ee7d8f6f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Cunha?= <j.a.cunha@gmail.com>
Date: Tue, 9 Apr 2019 20:45:58 +0100
Subject: [PATCH] Adapt functions to work for external Knative

Remove Kn services cache from Clusters::Application::Knative

Knative function can exist even if user did not installed Knative via
GitLab managed apps.

-> Move responsibility of finding services into the Cluster
-> Responsability is inside Clusters::Cluster::KnativeServiceFinder
-> Projects::Serverless::FunctionsFinder now calls depends solely on a
cluster to find the Kn services.
-> Detect Knative by resource presence instead of service presence
-> Mock knative_installed response temporarily for frontend to develop

Display loader while `installed === 'checking'`

Added frontend work to determine if Knative is installed

Memoize with_reactive_cache(*args, &block) to avoid race conditions

When calling with_reactive_cache more than once, it's possible that the
second call will already have the value populated. Therefore, in cases
where we need the sequential calls to have consistent results, we'd fall
under a race condition.

Check knative installation via Knative resource presence

Only load pods if Knative is discovered

Always return a response in FunctionsController#index

- Always indicate if Knative is installed, not installed or checking
- Always indicate the partial response for functions. Final response is
guaranteed when knative_installed is either true | false.

Adds specs for Clusters::Cluster#knative_services_finder

Fix method name when calling on specs

Add an explicit check for functions

Added an explicit check to see if there are any functions available

Fix Serverless feature spec

- we don't find knative installation via database anymore,
rather via Knative resource

Display error message for request timeouts

Display an error message if the request times out

Adds feature specs for when functions exist

Remove a test purposed hardcoded flag

Add ability to partially load functions

Added the ability to partially load functions on the frontend

Add frontend unit tests

Added tests for the new frontend additions

Generate new translations

Generated new frontend translations

Address review comments

Cleaned up the frontend unit test.
Added computed prop for `isInstalled`.

Move string to constant

Simplify nil to array conversion

Put knative_installed states in a frozen hash for better read

Pluralize list of Knative states

Quey services and pods filtering name

This way we don't need to filter the namespace in memory.
Also, the data we get from the network is much smaller.

Simplify cache_key and fix bug

- Simplifies the cache_key by removing namespace duplicate
- Fixes a bug with reactive_cache memoization
---
 .../serverless/components/functions.vue       |  36 ++++--
 .../javascripts/serverless/constants.js       |   4 +
 .../serverless/serverless_bundle.js           |   3 +-
 .../javascripts/serverless/store/actions.js   |  29 +++--
 .../serverless/store/mutation_types.js        |   2 +
 .../javascripts/serverless/store/mutations.js |  15 ++-
 .../javascripts/serverless/store/state.js     |   1 +
 .../serverless/functions_controller.rb        |  10 +-
 .../cluster/knative_services_finder.rb        | 114 ++++++++++++++++++
 .../projects/serverless/functions_finder.rb   |  39 +++---
 app/models/clusters/applications/knative.rb   |  48 --------
 app/models/clusters/cluster.rb                |   4 +
 ...ess-with-existing-knative-installation.yml |   5 +
 locale/gitlab.pot                             |   3 +
 .../serverless/functions_controller_spec.rb   |  92 +++++++++++---
 .../projects/serverless/functions_spec.rb     |  59 ++++++---
 .../cluster/knative_services_finder_spec.rb   | 105 ++++++++++++++++
 .../serverless/functions_finder_spec.rb       |  68 +++++++----
 .../components/environment_row_spec.js        |   8 +-
 .../serverless/components/functions_spec.js   |  27 ++++-
 spec/frontend/serverless/mock_data.js         | 110 +++++++++--------
 .../frontend/serverless/store/getters_spec.js |   2 +-
 .../serverless/store/mutations_spec.js        |   4 +-
 .../clusters/applications/knative_spec.rb     |  76 ------------
 spec/models/clusters/cluster_spec.rb          |   5 +
 spec/support/helpers/kubernetes_helpers.rb    |  46 +++++--
 26 files changed, 617 insertions(+), 298 deletions(-)
 create mode 100644 app/finders/clusters/cluster/knative_services_finder.rb
 create mode 100644 changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
 create mode 100644 spec/finders/clusters/cluster/knative_services_finder_spec.rb

diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index f9b4e78956380..94341050b861b 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
 import FunctionRow from './function_row.vue';
 import EnvironmentRow from './environment_row.vue';
 import EmptyState from './empty_state.vue';
+import { CHECKING_INSTALLED } from '../constants';
 
 export default {
   components: {
@@ -13,10 +14,6 @@ export default {
     GlLoadingIcon,
   },
   props: {
-    installed: {
-      type: Boolean,
-      required: true,
-    },
     clustersPath: {
       type: String,
       required: true,
@@ -31,8 +28,15 @@ export default {
     },
   },
   computed: {
-    ...mapState(['isLoading', 'hasFunctionData']),
+    ...mapState(['installed', 'isLoading', 'hasFunctionData']),
     ...mapGetters(['getFunctions']),
+
+    checkingInstalled() {
+      return this.installed === CHECKING_INSTALLED;
+    },
+    isInstalled() {
+      return this.installed === true;
+    },
   },
   created() {
     this.fetchFunctions({
@@ -47,15 +51,16 @@ export default {
 
 <template>
   <section id="serverless-functions">
-    <div v-if="installed">
+    <gl-loading-icon
+      v-if="checkingInstalled"
+      :size="2"
+      class="prepend-top-default append-bottom-default"
+    />
+
+    <div v-else-if="isInstalled">
       <div v-if="hasFunctionData">
-        <gl-loading-icon
-          v-if="isLoading"
-          :size="2"
-          class="prepend-top-default append-bottom-default"
-        />
-        <template v-else>
-          <div class="groups-list-tree-container">
+        <template>
+          <div class="groups-list-tree-container js-functions-wrapper">
             <ul class="content-list group-list-tree">
               <environment-row
                 v-for="(env, index) in getFunctions"
@@ -66,6 +71,11 @@ export default {
             </ul>
           </div>
         </template>
+        <gl-loading-icon
+          v-if="isLoading"
+          :size="2"
+          class="prepend-top-default append-bottom-default js-functions-loader"
+        />
       </div>
       <div v-else class="empty-state js-empty-state">
         <div class="text-content">
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
index 35f77205f2ce9..2fa15e56ccb67 100644
--- a/app/assets/javascripts/serverless/constants.js
+++ b/app/assets/javascripts/serverless/constants.js
@@ -1,3 +1,7 @@
 export const MAX_REQUESTS = 3; // max number of times to retry
 
 export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
+
+export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed
+
+export const TIMEOUT = 'timeout';
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 2d3f086ffeedf..ed3b633d76680 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -45,7 +45,7 @@ export default class Serverless {
         },
       });
     } else {
-      const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+      const { statusPath, clustersPath, helpPath } = document.querySelector(
         '.js-serverless-functions-page',
       ).dataset;
 
@@ -56,7 +56,6 @@ export default class Serverless {
         render(createElement) {
           return createElement(Functions, {
             props: {
-              installed: installed !== undefined,
               clustersPath,
               helpPath,
               statusPath,
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index 826501c90224c..a0a9fdf7ace1b 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils';
 import statusCodes from '~/lib/utils/http_status';
 import { backOff } from '~/lib/utils/common_utils';
 import createFlash from '~/flash';
-import { MAX_REQUESTS } from '../constants';
+import { __ } from '~/locale';
+import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
 
 export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
 export const receiveFunctionsSuccess = ({ commit }, data) =>
   commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
-export const receiveFunctionsNoDataSuccess = ({ commit }) =>
-  commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS);
+export const receiveFunctionsPartial = ({ commit }, data) =>
+  commit(types.RECEIVE_FUNCTIONS_PARTIAL, data);
+export const receiveFunctionsTimeout = ({ commit }, data) =>
+  commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data);
+export const receiveFunctionsNoDataSuccess = ({ commit }, data) =>
+  commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data);
 export const receiveFunctionsError = ({ commit }, error) =>
   commit(types.RECEIVE_FUNCTIONS_ERROR, error);
 
@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) =>
 export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
   let retryCount = 0;
 
+  const functionsPartiallyFetched = data => {
+    if (data.functions !== null && data.functions.length) {
+      dispatch('receiveFunctionsPartial', data);
+    }
+  };
+
   dispatch('requestFunctionsLoading');
 
   backOff((next, stop) => {
     axios
       .get(functionsPath)
       .then(response => {
-        if (response.status === statusCodes.NO_CONTENT) {
+        if (response.data.knative_installed === CHECKING_INSTALLED) {
           retryCount += 1;
           if (retryCount < MAX_REQUESTS) {
+            functionsPartiallyFetched(response.data);
             next();
           } else {
-            stop(null);
+            stop(TIMEOUT);
           }
         } else {
           stop(response.data);
@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
       .catch(stop);
   })
     .then(data => {
-      if (data !== null) {
+      if (data === TIMEOUT) {
+        dispatch('receiveFunctionsTimeout');
+        createFlash(__('Loading functions timed out. Please reload the page to try again.'));
+      } else if (data.functions !== null && data.functions.length) {
         dispatch('receiveFunctionsSuccess', data);
       } else {
-        dispatch('receiveFunctionsNoDataSuccess');
+        dispatch('receiveFunctionsNoDataSuccess', data);
       }
     })
     .catch(error => {
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
index 25b2f7ac38aa2..b8fa9ea1a013f 100644
--- a/app/assets/javascripts/serverless/store/mutation_types.js
+++ b/app/assets/javascripts/serverless/store/mutation_types.js
@@ -1,5 +1,7 @@
 export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
 export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
+export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL';
+export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT';
 export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
 export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
 
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
index 991f32a275d03..2685a5b11ffc8 100644
--- a/app/assets/javascripts/serverless/store/mutations.js
+++ b/app/assets/javascripts/serverless/store/mutations.js
@@ -5,12 +5,23 @@ export default {
     state.isLoading = true;
   },
   [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
-    state.functions = data;
+    state.functions = data.functions;
+    state.installed = data.knative_installed;
     state.isLoading = false;
     state.hasFunctionData = true;
   },
-  [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) {
+  [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) {
+    state.functions = data.functions;
+    state.installed = true;
+    state.isLoading = true;
+    state.hasFunctionData = true;
+  },
+  [types.RECEIVE_FUNCTIONS_TIMEOUT](state) {
+    state.isLoading = false;
+  },
+  [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) {
     state.isLoading = false;
+    state.installed = data.knative_installed;
     state.hasFunctionData = false;
   },
   [types.RECEIVE_FUNCTIONS_ERROR](state, error) {
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
index afc3f37d7ba3f..fdd292997493f 100644
--- a/app/assets/javascripts/serverless/store/state.js
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -1,5 +1,6 @@
 export default () => ({
   error: null,
+  installed: 'checking',
   isLoading: true,
 
   // functions
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 79030da64d348..4b0d001fca6a9 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -10,15 +10,13 @@ def index
           format.json do
             functions = finder.execute
 
-            if functions.any?
-              render json: serialize_function(functions)
-            else
-              head :no_content
-            end
+            render json: {
+              knative_installed: finder.knative_installed,
+              functions: serialize_function(functions)
+            }.to_json
           end
 
           format.html do
-            @installed = finder.installed?
             render
           end
         end
diff --git a/app/finders/clusters/cluster/knative_services_finder.rb b/app/finders/clusters/cluster/knative_services_finder.rb
new file mode 100644
index 0000000000000..85b0967e935b5
--- /dev/null
+++ b/app/finders/clusters/cluster/knative_services_finder.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+module Clusters
+  class Cluster
+    class KnativeServicesFinder
+      include ReactiveCaching
+      include Gitlab::Utils::StrongMemoize
+
+      KNATIVE_STATES = {
+        'checking' => 'checking',
+        'installed' => 'installed',
+        'not_found' => 'not_found'
+      }.freeze
+
+      self.reactive_cache_key = ->(finder) { finder.model_name }
+      self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
+
+      attr_reader :cluster, :project
+
+      def initialize(cluster, project)
+        @cluster = cluster
+        @project = project
+      end
+
+      def with_reactive_cache_memoized(*cache_args, &block)
+        strong_memoize(:reactive_cache) do
+          with_reactive_cache(*cache_args, &block)
+        end
+      end
+
+      def clear_cache!
+        clear_reactive_cache!(*cache_args)
+      end
+
+      def self.from_cache(cluster_id, project_id)
+        cluster = Clusters::Cluster.find(cluster_id)
+        project = ::Project.find(project_id)
+
+        new(cluster, project)
+      end
+
+      def calculate_reactive_cache(*)
+        # read_services calls knative_client.discover implicitily. If we stop
+        # detecting services but still want to detect knative, we'll need to
+        # explicitily call: knative_client.discover
+        #
+        # We didn't create it separately to avoid 2 cluster requests.
+        ksvc = read_services
+        pods = knative_client.discovered ? read_pods : []
+        { services: ksvc, pods: pods, knative_detected: knative_client.discovered }
+      end
+
+      def services
+        return [] unless search_namespace
+
+        cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+        cached_data.to_h.fetch(:services, [])
+      end
+
+      def cache_args
+        [cluster.id, project.id]
+      end
+
+      def service_pod_details(service)
+        cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+        cached_data.to_h.fetch(:pods, []).select do |pod|
+          filter_pods(pod, service)
+        end
+      end
+
+      def knative_detected
+        cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+
+        knative_state = cached_data.to_h[:knative_detected]
+
+        return KNATIVE_STATES['checking'] if knative_state.nil?
+        return KNATIVE_STATES['installed'] if knative_state
+
+        KNATIVE_STATES['uninstalled']
+      end
+
+      def model_name
+        self.class.name.underscore.tr('/', '_')
+      end
+
+      private
+
+      def search_namespace
+        @search_namespace ||= cluster.kubernetes_namespace_for(project)
+      end
+
+      def knative_client
+        cluster.kubeclient.knative_client
+      end
+
+      def filter_pods(pod, service)
+        pod["metadata"]["labels"]["serving.knative.dev/service"] == service
+      end
+
+      def read_services
+        knative_client.get_services(namespace: search_namespace).as_json
+      rescue Kubeclient::ResourceNotFoundError
+        []
+      end
+
+      def read_pods
+        cluster.kubeclient.core_client.get_pods(namespace: search_namespace).as_json
+      end
+
+      def id
+        nil
+      end
+    end
+  end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index e5bffccabfe05..9b8d7ed5a58ad 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -14,8 +14,15 @@ def execute
         knative_services.flatten.compact
       end
 
-      def installed?
-        clusters_with_knative_installed.exists?
+      # Possible return values: Clusters::Cluster::KnativeServicesFinder::KNATIVE_STATE
+      def knative_installed
+        states = @clusters.map do |cluster|
+          cluster.knative_services_finder(project).knative_detected.tap do |state|
+            return state if state == ::Clusters::Cluster::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
+          end
+        end
+
+        states.any? { |state| state == ::Clusters::Cluster::KnativeServicesFinder::KNATIVE_STATES['installed'] }
       end
 
       def service(environment_scope, name)
@@ -25,7 +32,7 @@ def service(environment_scope, name)
       def invocation_metrics(environment_scope, name)
         return unless prometheus_adapter&.can_query?
 
-        cluster = clusters_with_knative_installed.preload_knative.find do |c|
+        cluster = @clusters.find do |c|
           environment_scope == c.environment_scope
         end
 
@@ -34,7 +41,7 @@ def invocation_metrics(environment_scope, name)
       end
 
       def has_prometheus?(environment_scope)
-        clusters_with_knative_installed.preload_knative.to_a.any? do |cluster|
+        @clusters.any? do |cluster|
           environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
         end
       end
@@ -42,10 +49,12 @@ def has_prometheus?(environment_scope)
       private
 
       def knative_service(environment_scope, name)
-        clusters_with_knative_installed.preload_knative.map do |cluster|
+        @clusters.map do |cluster|
           next if environment_scope != cluster.environment_scope
 
-          services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+          services = cluster
+            .knative_services_finder(project)
+            .services
             .select { |svc| svc["metadata"]["name"] == name }
 
           add_metadata(cluster, services).first unless services.nil?
@@ -53,8 +62,11 @@ def knative_service(environment_scope, name)
       end
 
       def knative_services
-        clusters_with_knative_installed.preload_knative.map do |cluster|
-          services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+        @clusters.map do |cluster|
+          services = cluster
+            .knative_services_finder(project)
+            .services
+
           add_metadata(cluster, services) unless services.nil?
         end
       end
@@ -65,17 +77,14 @@ def add_metadata(cluster, services)
           s["cluster_id"] = cluster.id
 
           if services.length == 1
-            s["podcount"] = cluster.application_knative.service_pod_details(
-              cluster.kubernetes_namespace_for(project),
-              s["metadata"]["name"]).length
+            s["podcount"] = cluster
+              .knative_services_finder(project)
+              .service_pod_details(s["metadata"]["name"])
+              .length
           end
         end
       end
 
-      def clusters_with_knative_installed
-        @clusters.with_knative_installed
-      end
-
       # rubocop: disable CodeReuse/ServiceClass
       def prometheus_adapter
         @prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 9fbf5d8af045d..d5a3bd62e3d76 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,9 +15,6 @@ class Knative < ApplicationRecord
       include ::Clusters::Concerns::ApplicationVersion
       include ::Clusters::Concerns::ApplicationData
       include AfterCommitQueue
-      include ReactiveCaching
-
-      self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
 
       def set_initial_status
         return unless not_installable?
@@ -41,8 +38,6 @@ def set_initial_status
 
       scope :for_cluster, -> (cluster) { where(cluster: cluster) }
 
-      after_save :clear_reactive_cache!
-
       def chart
         'knative/knative'
       end
@@ -77,55 +72,12 @@ def schedule_status_update
         ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
       end
 
-      def client
-        cluster.kubeclient.knative_client
-      end
-
-      def services
-        with_reactive_cache do |data|
-          data[:services]
-        end
-      end
-
-      def calculate_reactive_cache
-        { services: read_services, pods: read_pods }
-      end
-
       def ingress_service
         cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system')
       end
 
-      def services_for(ns: namespace)
-        return [] unless services
-        return [] unless ns
-
-        services.select do |service|
-          service.dig('metadata', 'namespace') == ns
-        end
-      end
-
-      def service_pod_details(ns, service)
-        with_reactive_cache do |data|
-          data[:pods].select { |pod| filter_pods(pod, ns, service) }
-        end
-      end
-
       private
 
-      def read_pods
-        cluster.kubeclient.core_client.get_pods.as_json
-      end
-
-      def filter_pods(pod, namespace, service)
-        pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
-      end
-
-      def read_services
-        client.get_services.as_json
-      rescue Kubeclient::ResourceNotFoundError
-        []
-      end
-
       def install_knative_metrics
         ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
       end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 57a1e461b2d21..e1d6b2a802be6 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -223,6 +223,10 @@ def predefined_variables
       end
     end
 
+    def knative_services_finder(project)
+      @knative_services_finder ||= KnativeServicesFinder.new(self, project)
+    end
+
     private
 
     def instance_domain
diff --git a/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
new file mode 100644
index 0000000000000..53be008816d5d
--- /dev/null
+++ b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
@@ -0,0 +1,5 @@
+---
+title: Enable function features for external Knative installations
+merge_request: 27173
+author:
+type: changed
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4fbcab95420cd..c1e405716e121 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5742,6 +5742,9 @@ msgstr ""
 msgid "Live preview"
 msgstr ""
 
+msgid "Loading functions timed out. Please reload the page to try again."
+msgstr ""
+
 msgid "Loading the GitLab IDE..."
 msgstr ""
 
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 782f5f272d921..18c594acae030 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -8,9 +8,8 @@
 
   let(:user) { create(:user) }
   let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-  let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
   let(:service) { cluster.platform_kubernetes }
-  let(:project) { cluster.project}
+  let(:project) { cluster.project }
 
   let(:namespace) do
     create(:cluster_kubernetes_namespace,
@@ -30,17 +29,69 @@ def params(opts = {})
   end
 
   describe 'GET #index' do
-    context 'empty cache' do
-      it 'has no data' do
+    let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
+
+    context 'when cache is being read' do
+      let(:knative_state) { 'checking' }
+      let(:functions) { [] }
+
+      before do
         get :index, params: params({ format: :json })
+      end
 
-        expect(response).to have_gitlab_http_status(204)
+      it 'returns checking' do
+        expect(json_response).to eq expected_json
       end
 
-      it 'renders an html page' do
-        get :index, params: params
+      it { expect(response).to have_gitlab_http_status(200) }
+    end
+
+    context 'when cache is ready' do
+      let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+      let(:knative_state) { true }
 
-        expect(response).to have_gitlab_http_status(200)
+      before do
+        allow_any_instance_of(Clusters::Cluster)
+          .to receive(:knative_services_finder)
+          .and_return(knative_services_finder)
+        synchronous_reactive_cache(knative_services_finder)
+        stub_kubeclient_service_pods(
+          kube_response({ "kind" => "PodList", "items" => [] }),
+          namespace: namespace.namespace
+        )
+      end
+
+      context 'when no functions were found' do
+        let(:functions) { [] }
+
+        before do
+          stub_kubeclient_knative_services(
+            namespace: namespace.namespace,
+            response: kube_response({ "kind" => "ServiceList", "items" => [] })
+          )
+          get :index, params: params({ format: :json })
+        end
+
+        it 'returns checking' do
+          expect(json_response).to eq expected_json
+        end
+
+        it { expect(response).to have_gitlab_http_status(200) }
+      end
+
+      context 'when functions were found' do
+        let(:functions) { ["asdf"] }
+
+        before do
+          stub_kubeclient_knative_services(namespace: namespace.namespace)
+          get :index, params: params({ format: :json })
+        end
+
+        it 'returns functions' do
+          expect(json_response["functions"]).not_to be_empty
+        end
+
+        it { expect(response).to have_gitlab_http_status(200) }
       end
     end
   end
@@ -56,11 +107,12 @@ def params(opts = {})
     context 'valid data', :use_clean_rails_memory_store_caching do
       before do
         stub_kubeclient_service_pods
-        stub_reactive_cache(knative,
+        stub_reactive_cache(cluster.knative_services_finder(project),
           {
             services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
             pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          })
+          },
+          *cluster.knative_services_finder(project).cache_args)
       end
 
       it 'has a valid function name' do
@@ -88,11 +140,12 @@ def params(opts = {})
   describe 'GET #index with data', :use_clean_rails_memory_store_caching do
     before do
       stub_kubeclient_service_pods
-      stub_reactive_cache(knative,
+      stub_reactive_cache(cluster.knative_services_finder(project),
         {
           services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
           pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-        })
+        },
+        *cluster.knative_services_finder(project).cache_args)
     end
 
     it 'has data' do
@@ -100,11 +153,16 @@ def params(opts = {})
 
       expect(response).to have_gitlab_http_status(200)
 
-      expect(json_response).to contain_exactly(
-        a_hash_including(
-          "name" => project.name,
-          "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
-        )
+      expect(json_response).to match(
+        {
+          "knative_installed" => "checking",
+          "functions" => [
+            a_hash_including(
+              "name" => project.name,
+              "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+            )
+          ]
+        }
       )
     end
 
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index e14934b16724d..9865dbbfb3c3e 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -4,6 +4,7 @@
 
 describe 'Functions', :js do
   include KubernetesHelpers
+  include ReactiveCachingHelpers
 
   let(:project) { create(:project) }
   let(:user) { create(:user) }
@@ -13,44 +14,70 @@
     gitlab_sign_in(user)
   end
 
-  context 'when user does not have a cluster and visits the serverless page' do
+  shared_examples "it's missing knative installation" do
     before do
       visit project_serverless_functions_path(project)
     end
 
-    it 'sees an empty state' do
+    it 'sees an empty state require Knative installation' do
       expect(page).to have_link('Install Knative')
       expect(page).to have_selector('.empty-state')
     end
   end
 
+  context 'when user does not have a cluster and visits the serverless page' do
+    it_behaves_like "it's missing knative installation"
+  end
+
   context 'when the user does have a cluster and visits the serverless page' do
     let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
 
-    before do
-      visit project_serverless_functions_path(project)
-    end
-
-    it 'sees an empty state' do
-      expect(page).to have_link('Install Knative')
-      expect(page).to have_selector('.empty-state')
-    end
+    it_behaves_like "it's missing knative installation"
   end
 
   context 'when the user has a cluster and knative installed and visits the serverless page' do
     let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
     let(:service) { cluster.platform_kubernetes }
-    let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
-    let(:project) { knative.cluster.project }
+    let(:project) { cluster.project }
+    let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+    let(:namespace) do
+      create(:cluster_kubernetes_namespace,
+        cluster: cluster,
+        cluster_project: cluster.cluster_project,
+        project: cluster.cluster_project.project)
+    end
 
     before do
-      stub_kubeclient_knative_services
-      stub_kubeclient_service_pods
+      allow_any_instance_of(Clusters::Cluster)
+        .to receive(:knative_services_finder)
+        .and_return(knative_services_finder)
+      synchronous_reactive_cache(knative_services_finder)
+      stub_kubeclient_knative_services(stub_get_services_options)
+      stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
       visit project_serverless_functions_path(project)
     end
 
-    it 'sees an empty listing of serverless functions' do
-      expect(page).to have_selector('.empty-state')
+    context 'when there are no functions' do
+      let(:stub_get_services_options) do
+        {
+          namespace: namespace.namespace,
+          response: kube_response({ "kind" => "ServiceList", "items" => [] })
+        }
+      end
+
+      it 'sees an empty listing of serverless functions' do
+        expect(page).to have_selector('.empty-state')
+        expect(page).not_to have_selector('.content-list')
+      end
+    end
+
+    context 'when there are functions' do
+      let(:stub_get_services_options) { { namespace: namespace.namespace } }
+
+      it 'does not see an empty listing of serverless functions' do
+        expect(page).not_to have_selector('.empty-state')
+        expect(page).to have_selector('.content-list')
+      end
     end
   end
 end
diff --git a/spec/finders/clusters/cluster/knative_services_finder_spec.rb b/spec/finders/clusters/cluster/knative_services_finder_spec.rb
new file mode 100644
index 0000000000000..277200d06f497
--- /dev/null
+++ b/spec/finders/clusters/cluster/knative_services_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Cluster::KnativeServicesFinder do
+  include KubernetesHelpers
+  include ReactiveCachingHelpers
+
+  let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+  let(:service) { cluster.platform_kubernetes }
+  let(:project) { cluster.cluster_project.project }
+  let(:namespace) do
+    create(:cluster_kubernetes_namespace,
+      cluster: cluster,
+      cluster_project: cluster.cluster_project,
+      project: project)
+  end
+
+  before do
+    stub_kubeclient_knative_services(namespace: namespace.namespace)
+    stub_kubeclient_service_pods(
+      kube_response(
+        kube_knative_pods_body(
+          project.name, namespace.namespace
+        )
+      ),
+      namespace: namespace.namespace
+    )
+  end
+
+  shared_examples 'a cached data' do
+    it 'has an unintialized cache' do
+      is_expected.to be_blank
+    end
+
+    context 'when using synchronous reactive cache' do
+      before do
+        synchronous_reactive_cache(cluster.knative_services_finder(project))
+      end
+
+      context 'when there are functions for cluster namespace' do
+        it { is_expected.not_to be_blank }
+      end
+
+      context 'when there are no functions for cluster namespace' do
+        before do
+          stub_kubeclient_knative_services(
+            namespace: namespace.namespace,
+            response: kube_response({ "kind" => "ServiceList", "items" => [] })
+          )
+          stub_kubeclient_service_pods(
+            kube_response({ "kind" => "PodList", "items" => [] }),
+            namespace: namespace.namespace
+          )
+        end
+
+        it { is_expected.to be_blank }
+      end
+    end
+  end
+
+  describe '#service_pod_details' do
+    subject { cluster.knative_services_finder(project).service_pod_details(project.name) }
+
+    it_behaves_like 'a cached data'
+  end
+
+  describe '#services' do
+    subject { cluster.knative_services_finder(project).services }
+
+    it_behaves_like 'a cached data'
+  end
+
+  describe '#knative_detected' do
+    subject { cluster.knative_services_finder(project).knative_detected }
+    before do
+      synchronous_reactive_cache(cluster.knative_services_finder(project))
+    end
+
+    context 'when knative is installed' do
+      before do
+        stub_kubeclient_discover(service.api_url)
+      end
+
+      it { is_expected.to be_truthy }
+      it "discovers knative installation" do
+        expect { subject }
+          .to change { cluster.kubeclient.knative_client.discovered }
+          .from(false)
+          .to(true)
+      end
+    end
+
+    context 'when knative is not installed' do
+      before do
+        stub_kubeclient_discover_knative_not_found(service.api_url)
+      end
+
+      it { is_expected.to be_falsy }
+      it "does not discover knative installation" do
+        expect { subject }.not_to change { cluster.kubeclient.knative_client.discovered }
+      end
+    end
+  end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 3ad38207da4a2..8aea45b457c24 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -10,7 +10,7 @@
   let(:user) { create(:user) }
   let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
   let(:service) { cluster.platform_kubernetes }
-  let(:project) { cluster.project}
+  let(:project) { cluster.project }
 
   let(:namespace) do
     create(:cluster_kubernetes_namespace,
@@ -23,9 +23,45 @@
     project.add_maintainer(user)
   end
 
+  describe '#installed' do
+    it 'when reactive_caching is still fetching data' do
+      expect(described_class.new(project).knative_installed).to eq 'checking'
+    end
+
+    context 'when reactive_caching has finished' do
+      let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+      before do
+        allow_any_instance_of(Clusters::Cluster)
+          .to receive(:knative_services_finder)
+          .and_return(knative_services_finder)
+        synchronous_reactive_cache(knative_services_finder)
+      end
+
+      context 'when knative is not installed' do
+        it 'returns false' do
+          stub_kubeclient_discover_knative_not_found(service.api_url)
+
+          expect(described_class.new(project).knative_installed).to eq false
+        end
+      end
+
+      context 'reactive_caching is finished and knative is installed' do
+        let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+        it 'returns true' do
+          stub_kubeclient_knative_services(namespace: namespace.namespace)
+          stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
+
+          expect(described_class.new(project).knative_installed).to be true
+        end
+      end
+    end
+  end
+
   describe 'retrieve data from knative' do
-    it 'does not have knative installed' do
-      expect(described_class.new(project).execute).to be_empty
+    context 'does not have knative installed' do
+      it { expect(described_class.new(project).execute).to be_empty }
     end
 
     context 'has knative installed' do
@@ -38,22 +74,24 @@
 
       it 'there are functions', :use_clean_rails_memory_store_caching do
         stub_kubeclient_service_pods
-        stub_reactive_cache(knative,
+        stub_reactive_cache(cluster.knative_services_finder(project),
           {
             services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
             pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          })
+          },
+          *cluster.knative_services_finder(project).cache_args)
 
         expect(finder.execute).not_to be_empty
       end
 
       it 'has a function', :use_clean_rails_memory_store_caching do
         stub_kubeclient_service_pods
-        stub_reactive_cache(knative,
+        stub_reactive_cache(cluster.knative_services_finder(project),
           {
             services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
             pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
-          })
+          },
+          *cluster.knative_services_finder(project).cache_args)
 
         result = finder.service(cluster.environment_scope, cluster.project.name)
         expect(result).not_to be_empty
@@ -84,20 +122,4 @@
       end
     end
   end
-
-  describe 'verify if knative is installed' do
-    context 'knative is not installed' do
-      it 'does not have knative installed' do
-        expect(described_class.new(project).installed?).to be false
-      end
-    end
-
-    context 'knative is installed' do
-      let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
-
-      it 'does have knative installed' do
-        expect(described_class.new(project).installed?).to be true
-      end
-    end
-  end
 end
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index 161a637dd75a3..0ad85e218dc3e 100644
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -14,7 +14,7 @@ describe('environment row component', () => {
 
     beforeEach(() => {
       localVue = createLocalVue();
-      vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*');
+      vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
     });
 
     afterEach(() => vm.$destroy());
@@ -48,7 +48,11 @@ describe('environment row component', () => {
 
     beforeEach(() => {
       localVue = createLocalVue();
-      vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test');
+      vm = createComponent(
+        localVue,
+        translate(mockServerlessFunctionsDiffEnv.functions).test,
+        'test',
+      );
     });
 
     afterEach(() => vm.$destroy());
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 6924fb9e91f8b..d8a80f8031e2c 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -34,11 +34,11 @@ describe('functionsComponent', () => {
   });
 
   it('should render empty state when Knative is not installed', () => {
+    store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
     component = shallowMount(functionsComponent, {
       localVue,
       store,
       propsData: {
-        installed: false,
         clustersPath: '',
         helpPath: '',
         statusPath: '',
@@ -55,7 +55,6 @@ describe('functionsComponent', () => {
       localVue,
       store,
       propsData: {
-        installed: true,
         clustersPath: '',
         helpPath: '',
         statusPath: '',
@@ -67,12 +66,11 @@ describe('functionsComponent', () => {
   });
 
   it('should render empty state when there is no function data', () => {
-    store.dispatch('receiveFunctionsNoDataSuccess');
+    store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
     component = shallowMount(functionsComponent, {
       localVue,
       store,
       propsData: {
-        installed: true,
         clustersPath: '',
         helpPath: '',
         statusPath: '',
@@ -91,12 +89,31 @@ describe('functionsComponent', () => {
     );
   });
 
+  it('should render functions and a loader when functions are partially fetched', () => {
+    store.dispatch('receiveFunctionsPartial', {
+      ...mockServerlessFunctions,
+      knative_installed: 'checking',
+    });
+    component = shallowMount(functionsComponent, {
+      localVue,
+      store,
+      propsData: {
+        clustersPath: '',
+        helpPath: '',
+        statusPath: '',
+      },
+      sync: false,
+    });
+
+    expect(component.find('.js-functions-wrapper').exists()).toBe(true);
+    expect(component.find('.js-functions-loader').exists()).toBe(true);
+  });
+
   it('should render the functions list', () => {
     component = shallowMount(functionsComponent, {
       localVue,
       store,
       propsData: {
-        installed: true,
         clustersPath: 'clustersPath',
         helpPath: 'helpPath',
         statusPath,
diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js
index a2c1861632435..ef616ceb37f41 100644
--- a/spec/frontend/serverless/mock_data.js
+++ b/spec/frontend/serverless/mock_data.js
@@ -1,56 +1,62 @@
-export const mockServerlessFunctions = [
-  {
-    name: 'testfunc1',
-    namespace: 'tm-example',
-    environment_scope: '*',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc1.tm-example.apps.example.com',
-    description: 'A test service',
-    image: 'knative-test-container-buildtemplate',
-  },
-  {
-    name: 'testfunc2',
-    namespace: 'tm-example',
-    environment_scope: '*',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc2.tm-example.apps.example.com',
-    description: 'A second test service\nThis one with additional descriptions',
-    image: 'knative-test-echo-buildtemplate',
-  },
-];
+export const mockServerlessFunctions = {
+  knative_installed: true,
+  functions: [
+    {
+      name: 'testfunc1',
+      namespace: 'tm-example',
+      environment_scope: '*',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc1.tm-example.apps.example.com',
+      description: 'A test service',
+      image: 'knative-test-container-buildtemplate',
+    },
+    {
+      name: 'testfunc2',
+      namespace: 'tm-example',
+      environment_scope: '*',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc2.tm-example.apps.example.com',
+      description: 'A second test service\nThis one with additional descriptions',
+      image: 'knative-test-echo-buildtemplate',
+    },
+  ],
+};
 
-export const mockServerlessFunctionsDiffEnv = [
-  {
-    name: 'testfunc1',
-    namespace: 'tm-example',
-    environment_scope: '*',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc1.tm-example.apps.example.com',
-    description: 'A test service',
-    image: 'knative-test-container-buildtemplate',
-  },
-  {
-    name: 'testfunc2',
-    namespace: 'tm-example',
-    environment_scope: 'test',
-    cluster_id: 46,
-    detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
-    podcount: null,
-    created_at: '2019-02-05T01:01:23Z',
-    url: 'http://testfunc2.tm-example.apps.example.com',
-    description: 'A second test service\nThis one with additional descriptions',
-    image: 'knative-test-echo-buildtemplate',
-  },
-];
+export const mockServerlessFunctionsDiffEnv = {
+  knative_installed: true,
+  functions: [
+    {
+      name: 'testfunc1',
+      namespace: 'tm-example',
+      environment_scope: '*',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc1.tm-example.apps.example.com',
+      description: 'A test service',
+      image: 'knative-test-container-buildtemplate',
+    },
+    {
+      name: 'testfunc2',
+      namespace: 'tm-example',
+      environment_scope: 'test',
+      cluster_id: 46,
+      detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+      podcount: null,
+      created_at: '2019-02-05T01:01:23Z',
+      url: 'http://testfunc2.tm-example.apps.example.com',
+      description: 'A second test service\nThis one with additional descriptions',
+      image: 'knative-test-echo-buildtemplate',
+    },
+  ],
+};
 
 export const mockServerlessFunction = {
   name: 'testfunc1',
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
index fb549c8f153fc..92853fda37c4a 100644
--- a/spec/frontend/serverless/store/getters_spec.js
+++ b/spec/frontend/serverless/store/getters_spec.js
@@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => {
 
   describe('getFunctions', () => {
     it('should translate the raw function array to group the functions per environment scope', () => {
-      state.functions = mockServerlessFunctions;
+      state.functions = mockServerlessFunctions.functions;
 
       const funcs = getters.getFunctions(state);
 
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
index ca3053e5c384f..e2771c7e5fd47 100644
--- a/spec/frontend/serverless/store/mutations_spec.js
+++ b/spec/frontend/serverless/store/mutations_spec.js
@@ -19,13 +19,13 @@ describe('ServerlessMutations', () => {
 
       expect(state.isLoading).toEqual(false);
       expect(state.hasFunctionData).toEqual(true);
-      expect(state.functions).toEqual(mockServerlessFunctions);
+      expect(state.functions).toEqual(mockServerlessFunctions.functions);
     });
 
     it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
       const state = {};
 
-      mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state);
+      mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true });
 
       expect(state.isLoading).toEqual(false);
       expect(state.hasFunctionData).toEqual(false);
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d5974f4719076..b38cf96de7ebe 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -3,9 +3,6 @@
 require 'rails_helper'
 
 describe Clusters::Applications::Knative do
-  include KubernetesHelpers
-  include ReactiveCachingHelpers
-
   let(:knative) { create(:clusters_applications_knative) }
 
   include_examples 'cluster application core specs', :clusters_applications_knative
@@ -146,77 +143,4 @@
   describe 'validations' do
     it { is_expected.to validate_presence_of(:hostname) }
   end
-
-  describe '#service_pod_details' do
-    let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-    let(:service) { cluster.platform_kubernetes }
-    let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
-    let(:namespace) do
-      create(:cluster_kubernetes_namespace,
-        cluster: cluster,
-        cluster_project: cluster.cluster_project,
-        project: cluster.cluster_project.project)
-    end
-
-    before do
-      stub_kubeclient_discover(service.api_url)
-      stub_kubeclient_knative_services
-      stub_kubeclient_service_pods
-      stub_reactive_cache(knative,
-        {
-          services: kube_response(kube_knative_services_body),
-          pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
-        })
-      synchronous_reactive_cache(knative)
-    end
-
-    it 'is able k8s core for pod details' do
-      expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
-    end
-  end
-
-  describe '#services' do
-    let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-    let(:service) { cluster.platform_kubernetes }
-    let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
-    let(:namespace) do
-      create(:cluster_kubernetes_namespace,
-        cluster: cluster,
-        cluster_project: cluster.cluster_project,
-        project: cluster.cluster_project.project)
-    end
-
-    subject { knative.services }
-
-    before do
-      stub_kubeclient_discover(service.api_url)
-      stub_kubeclient_knative_services
-      stub_kubeclient_service_pods
-    end
-
-    it 'has an unintialized cache' do
-      is_expected.to be_nil
-    end
-
-    context 'when using synchronous reactive cache' do
-      before do
-        stub_reactive_cache(knative,
-          {
-            services: kube_response(kube_knative_services_body),
-            pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
-          })
-        synchronous_reactive_cache(knative)
-      end
-
-      it 'has cached services' do
-        is_expected.not_to be_nil
-      end
-
-      it 'matches our namespace' do
-        expect(knative.services_for(ns: namespace)).not_to be_nil
-      end
-    end
-  end
 end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 4739e62289acd..60a19ccd48aa5 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -38,6 +38,11 @@
 
   it { is_expected.to respond_to :project }
 
+  it do
+    expect(subject.knative_services_finder(subject.project))
+      .to be_instance_of(Clusters::Cluster::KnativeServicesFinder)
+  end
+
   describe '.enabled' do
     subject { described_class.enabled }
 
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 78b7ae9c00c84..011c4df0fe5f3 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -17,17 +17,38 @@ def kube_deployments_response
     kube_response(kube_deployments_body)
   end
 
-  def stub_kubeclient_discover(api_url)
+  def stub_kubeclient_discover_base(api_url)
     WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
-    WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
-    WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
-    WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+    WebMock
+      .stub_request(:get, api_url + '/apis/extensions/v1beta1')
+      .to_return(kube_response(kube_v1beta1_discovery_body))
+    WebMock
+      .stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
+      .to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+  end
+
+  def stub_kubeclient_discover(api_url)
+    stub_kubeclient_discover_base(api_url)
+
+    WebMock
+      .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+      .to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+  end
+
+  def stub_kubeclient_discover_knative_not_found(api_url)
+    stub_kubeclient_discover_base(api_url)
+
+    WebMock
+      .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+      .to_return(status: [404, "Resource Not Found"])
   end
 
-  def stub_kubeclient_service_pods(status: nil)
+  def stub_kubeclient_service_pods(response = nil, options = {})
     stub_kubeclient_discover(service.api_url)
-    pods_url = service.api_url + "/api/v1/pods"
-    response = { status: status } if status
+
+    namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
+    pods_url = service.api_url + "/api/v1/#{namespace_path}pods"
 
     WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
   end
@@ -56,15 +77,18 @@ def stub_kubeclient_deployments(namespace, status: nil)
     WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
   end
 
-  def stub_kubeclient_knative_services(**options)
+  def stub_kubeclient_knative_services(options = {})
+    namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
     options[:name] ||= "kubetest"
-    options[:namespace] ||= "default"
     options[:domain] ||= "example.com"
+    options[:response] ||= kube_response(kube_knative_services_body(options))
 
     stub_kubeclient_discover(service.api_url)
-    knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
 
-    WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+    knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/#{namespace_path}services"
+
+    WebMock.stub_request(:get, knative_url).to_return(options[:response])
   end
 
   def stub_kubeclient_get_secret(api_url, **options)
-- 
GitLab