Commit 0b864208 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '58941-use-gitlab-serverless-with-existing-knative-installation' into 'master'

Resolve "Use GitLab serverless with existing Knative installation"

Closes #58941

See merge request gitlab-org/gitlab-ce!27173
parents aa6b8c8c 82e952a8
......@@ -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">
......
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';
......@@ -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,
......
......@@ -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 => {
......
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';
......
......@@ -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) {
......
export default () => ({
error: null,
installed: 'checking',
isLoading: true,
// functions
......
......@@ -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
......
# frozen_string_literal: true
module Clusters
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
......@@ -14,8 +14,16 @@ module Projects
knative_services.flatten.compact
end
def installed?
clusters_with_knative_installed.exists?
# Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
def knative_installed
states = @clusters.map do |cluster|
cluster.application_knative
cluster.knative_services_finder(project).knative_detected.tap do |state|
return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] }
end
def service(environment_scope, name)
......@@ -25,7 +33,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 +42,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 +50,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 +63,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 +78,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
......
......@@ -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
......
......@@ -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
......
---
title: Enable function features for external Knative installations
merge_request: 27173
author:
type: changed
......@@ -5862,6 +5862,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 ""
......
......@@ -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
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 @@ 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
......
......@@ -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
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::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
......@@ -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
......@@ -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());
......
......@@ -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,
......
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',
......
......@@ -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);
......
......@@ -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);
......
......@@ -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
......@@ -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::KnativeServicesFinder)
end
describe '.enabled' do
subject { described_class.enabled }
......
......@@ -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_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 @@ 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)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment