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
This commit is contained in:
João Cunha 2019-04-09 20:45:58 +01:00
parent 328740c613
commit a2aa160cea
26 changed files with 618 additions and 299 deletions

View File

@ -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">

View File

@ -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';

View File

@ -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,

View File

@ -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 => {

View File

@ -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';

View File

@ -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) {

View File

@ -1,5 +1,6 @@
export default () => ({
error: null,
installed: 'checking',
isLoading: true,
// functions

View File

@ -10,15 +10,13 @@ module Projects
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

View File

@ -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

View File

@ -14,8 +14,15 @@ module Projects
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 @@ module Projects
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 @@ module Projects
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 @@ module Projects
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 @@ module Projects
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 @@ module Projects
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

View File

@ -15,9 +15,6 @@ module Clusters
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 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) }
after_save :clear_reactive_cache!
def chart
'knative/knative'
end
@ -77,55 +72,12 @@ module Clusters
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

View File

@ -223,6 +223,10 @@ module Clusters
end
end
def knative_services_finder(project)
@knative_services_finder ||= KnativeServicesFinder.new(self, project)
end
private
def instance_domain

View File

@ -0,0 +1,5 @@
---
title: Enable function features for external Knative installations
merge_request: 27173
author:
type: changed

View File

@ -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 ""

View File

@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do
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 @@ describe Projects::Serverless::FunctionsController do
end
describe 'GET #index' do
context 'empty cache' do
it 'has no data' do
get :index, params: params({ format: :json })
let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
expect(response).to have_gitlab_http_status(204)
context 'when cache is being read' do
let(:knative_state) { 'checking' }
let(:functions) { [] }
before do
get :index, params: params({ format: :json })
end
it 'renders an html page' do
get :index, params: params
it 'returns checking' do
expect(json_response).to eq expected_json
end
expect(response).to have_gitlab_http_status(200)
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 }
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 @@ describe Projects::Serverless::FunctionsController do
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 @@ describe Projects::Serverless::FunctionsController do
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 @@ describe Projects::Serverless::FunctionsController do
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

View File

@ -4,6 +4,7 @@ require 'spec_helper'
describe 'Functions', :js do
include KubernetesHelpers
include ReactiveCachingHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
@ -13,44 +14,70 @@ describe 'Functions', :js do
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

View File

@ -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

View File

@ -10,7 +10,7 @@ describe Projects::Serverless::FunctionsFinder do
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 @@ describe Projects::Serverless::FunctionsFinder do
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 @@ describe Projects::Serverless::FunctionsFinder do
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 @@ describe Projects::Serverless::FunctionsFinder do
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

View File

@ -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());

View File

@ -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,

View File

@ -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',

View File

@ -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);

View File

@ -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);

View File

@ -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 Clusters::Applications::Knative do
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

View File

@ -38,6 +38,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
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 }

View File

@ -17,17 +17,38 @@ module KubernetesHelpers
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_service_pods(status: nil)
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(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 @@ module KubernetesHelpers
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)