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'; ...@@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import FunctionRow from './function_row.vue'; import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue'; import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import { CHECKING_INSTALLED } from '../constants';
export default { export default {
components: { components: {
...@@ -13,10 +14,6 @@ export default { ...@@ -13,10 +14,6 @@ export default {
GlLoadingIcon, GlLoadingIcon,
}, },
props: { props: {
installed: {
type: Boolean,
required: true,
},
clustersPath: { clustersPath: {
type: String, type: String,
required: true, required: true,
...@@ -31,8 +28,15 @@ export default { ...@@ -31,8 +28,15 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['isLoading', 'hasFunctionData']), ...mapState(['installed', 'isLoading', 'hasFunctionData']),
...mapGetters(['getFunctions']), ...mapGetters(['getFunctions']),
checkingInstalled() {
return this.installed === CHECKING_INSTALLED;
},
isInstalled() {
return this.installed === true;
},
}, },
created() { created() {
this.fetchFunctions({ this.fetchFunctions({
...@@ -47,15 +51,16 @@ export default { ...@@ -47,15 +51,16 @@ export default {
<template> <template>
<section id="serverless-functions"> <section id="serverless-functions">
<div v-if="installed">
<div v-if="hasFunctionData">
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="checkingInstalled"
:size="2" :size="2"
class="prepend-top-default append-bottom-default" class="prepend-top-default append-bottom-default"
/> />
<template v-else>
<div class="groups-list-tree-container"> <div v-else-if="isInstalled">
<div v-if="hasFunctionData">
<template>
<div class="groups-list-tree-container js-functions-wrapper">
<ul class="content-list group-list-tree"> <ul class="content-list group-list-tree">
<environment-row <environment-row
v-for="(env, index) in getFunctions" v-for="(env, index) in getFunctions"
...@@ -66,6 +71,11 @@ export default { ...@@ -66,6 +71,11 @@ export default {
</ul> </ul>
</div> </div>
</template> </template>
<gl-loading-icon
v-if="isLoading"
:size="2"
class="prepend-top-default append-bottom-default js-functions-loader"
/>
</div> </div>
<div v-else class="empty-state js-empty-state"> <div v-else class="empty-state js-empty-state">
<div class="text-content"> <div class="text-content">
......
export const MAX_REQUESTS = 3; // max number of times to retry 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 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 { ...@@ -45,7 +45,7 @@ export default class Serverless {
}, },
}); });
} else { } else {
const { statusPath, clustersPath, helpPath, installed } = document.querySelector( const { statusPath, clustersPath, helpPath } = document.querySelector(
'.js-serverless-functions-page', '.js-serverless-functions-page',
).dataset; ).dataset;
...@@ -56,7 +56,6 @@ export default class Serverless { ...@@ -56,7 +56,6 @@ export default class Serverless {
render(createElement) { render(createElement) {
return createElement(Functions, { return createElement(Functions, {
props: { props: {
installed: installed !== undefined,
clustersPath, clustersPath,
helpPath, helpPath,
statusPath, statusPath,
......
...@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils'; import { backOff } from '~/lib/utils/common_utils';
import createFlash from '~/flash'; 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 requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
export const receiveFunctionsSuccess = ({ commit }, data) => export const receiveFunctionsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_FUNCTIONS_SUCCESS, data); commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
export const receiveFunctionsNoDataSuccess = ({ commit }) => export const receiveFunctionsPartial = ({ commit }, data) =>
commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS); 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) => export const receiveFunctionsError = ({ commit }, error) =>
commit(types.RECEIVE_FUNCTIONS_ERROR, error); commit(types.RECEIVE_FUNCTIONS_ERROR, error);
...@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) => ...@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) =>
export const fetchFunctions = ({ dispatch }, { functionsPath }) => { export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
let retryCount = 0; let retryCount = 0;
const functionsPartiallyFetched = data => {
if (data.functions !== null && data.functions.length) {
dispatch('receiveFunctionsPartial', data);
}
};
dispatch('requestFunctionsLoading'); dispatch('requestFunctionsLoading');
backOff((next, stop) => { backOff((next, stop) => {
axios axios
.get(functionsPath) .get(functionsPath)
.then(response => { .then(response => {
if (response.status === statusCodes.NO_CONTENT) { if (response.data.knative_installed === CHECKING_INSTALLED) {
retryCount += 1; retryCount += 1;
if (retryCount < MAX_REQUESTS) { if (retryCount < MAX_REQUESTS) {
functionsPartiallyFetched(response.data);
next(); next();
} else { } else {
stop(null); stop(TIMEOUT);
} }
} else { } else {
stop(response.data); stop(response.data);
...@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { ...@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
.catch(stop); .catch(stop);
}) })
.then(data => { .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); dispatch('receiveFunctionsSuccess', data);
} else { } else {
dispatch('receiveFunctionsNoDataSuccess'); dispatch('receiveFunctionsNoDataSuccess', data);
} }
}) })
.catch(error => { .catch(error => {
......
export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING'; export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS'; 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_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR'; export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
......
...@@ -5,12 +5,23 @@ export default { ...@@ -5,12 +5,23 @@ export default {
state.isLoading = true; state.isLoading = true;
}, },
[types.RECEIVE_FUNCTIONS_SUCCESS](state, data) { [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
state.functions = data; state.functions = data.functions;
state.installed = data.knative_installed;
state.isLoading = false; state.isLoading = false;
state.hasFunctionData = true; 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.isLoading = false;
state.installed = data.knative_installed;
state.hasFunctionData = false; state.hasFunctionData = false;
}, },
[types.RECEIVE_FUNCTIONS_ERROR](state, error) { [types.RECEIVE_FUNCTIONS_ERROR](state, error) {
......
export default () => ({ export default () => ({
error: null, error: null,
installed: 'checking',
isLoading: true, isLoading: true,
// functions // functions
......
...@@ -10,15 +10,13 @@ module Projects ...@@ -10,15 +10,13 @@ module Projects
format.json do format.json do
functions = finder.execute functions = finder.execute
if functions.any? render json: {
render json: serialize_function(functions) knative_installed: finder.knative_installed,
else functions: serialize_function(functions)
head :no_content }.to_json
end
end end
format.html do format.html do
@installed = finder.installed?
render render
end end
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 ...@@ -14,8 +14,16 @@ module Projects
knative_services.flatten.compact knative_services.flatten.compact
end end
def installed? # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
clusters_with_knative_installed.exists? 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 end
def service(environment_scope, name) def service(environment_scope, name)
...@@ -25,7 +33,7 @@ module Projects ...@@ -25,7 +33,7 @@ module Projects
def invocation_metrics(environment_scope, name) def invocation_metrics(environment_scope, name)
return unless prometheus_adapter&.can_query? 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 environment_scope == c.environment_scope
end end
...@@ -34,7 +42,7 @@ module Projects ...@@ -34,7 +42,7 @@ module Projects
end end
def has_prometheus?(environment_scope) 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? environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
end end
end end
...@@ -42,10 +50,12 @@ module Projects ...@@ -42,10 +50,12 @@ module Projects
private private
def knative_service(environment_scope, name) 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 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 } .select { |svc| svc["metadata"]["name"] == name }
add_metadata(cluster, services).first unless services.nil? add_metadata(cluster, services).first unless services.nil?
...@@ -53,8 +63,11 @@ module Projects ...@@ -53,8 +63,11 @@ module Projects
end end
def knative_services def knative_services
clusters_with_knative_installed.preload_knative.map do |cluster| @clusters.map do |cluster|
services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project)) services = cluster
.knative_services_finder(project)
.services
add_metadata(cluster, services) unless services.nil? add_metadata(cluster, services) unless services.nil?
end end
end end
...@@ -65,15 +78,12 @@ module Projects ...@@ -65,15 +78,12 @@ module Projects
s["cluster_id"] = cluster.id s["cluster_id"] = cluster.id
if services.length == 1 if services.length == 1
s["podcount"] = cluster.application_knative.service_pod_details( s["podcount"] = cluster
cluster.kubernetes_namespace_for(project), .knative_services_finder(project)
s["metadata"]["name"]).length .service_pod_details(s["metadata"]["name"])
end .length
end end
end end
def clusters_with_knative_installed
@clusters.with_knative_installed
end end
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
......
...@@ -15,9 +15,6 @@ module Clusters ...@@ -15,9 +15,6 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue include AfterCommitQueue
include ReactiveCaching
self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
def set_initial_status def set_initial_status
return unless not_installable? return unless not_installable?
...@@ -41,8 +38,6 @@ module Clusters ...@@ -41,8 +38,6 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) } scope :for_cluster, -> (cluster) { where(cluster: cluster) }
after_save :clear_reactive_cache!
def chart def chart
'knative/knative' 'knative/knative'
end end
...@@ -77,55 +72,12 @@ module Clusters ...@@ -77,55 +72,12 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id) ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end 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 def ingress_service
cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system') cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system')
end 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 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 def install_knative_metrics
["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available? ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
end end
......
...@@ -223,6 +223,10 @@ module Clusters ...@@ -223,6 +223,10 @@ module Clusters
end end
end end
def knative_services_finder(project)
@knative_services_finder ||= KnativeServicesFinder.new(self, project)
end
private private
def instance_domain def instance_domain
......
---
title: Enable function features for external Knative installations
merge_request: 27173
author:
type: changed
...@@ -5862,6 +5862,9 @@ msgstr "" ...@@ -5862,6 +5862,9 @@ msgstr ""
msgid "Live preview" msgid "Live preview"
msgstr "" msgstr ""
msgid "Loading functions timed out. Please reload the page to try again."
msgstr ""
msgid "Loading the GitLab IDE..." msgid "Loading the GitLab IDE..."
msgstr "" msgstr ""
......
...@@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do ...@@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project} let(:project) { cluster.project }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
...@@ -30,17 +29,69 @@ describe Projects::Serverless::FunctionsController do ...@@ -30,17 +29,69 @@ describe Projects::Serverless::FunctionsController do
end end
describe 'GET #index' do describe 'GET #index' do
context 'empty cache' do let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
it 'has no data' do
context 'when cache is being read' do
let(:knative_state) { 'checking' }
let(:functions) { [] }
before do
get :index, params: params({ format: :json }) 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 end
it 'renders an html page' do it { expect(response).to have_gitlab_http_status(200) }
get :index, params: params end
expect(response).to have_gitlab_http_status(200) 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 end
end end
...@@ -56,11 +107,12 @@ describe Projects::Serverless::FunctionsController do ...@@ -56,11 +107,12 @@ describe Projects::Serverless::FunctionsController do
context 'valid data', :use_clean_rails_memory_store_caching do context 'valid data', :use_clean_rails_memory_store_caching do
before do before do
stub_kubeclient_service_pods 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"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
}) },
*cluster.knative_services_finder(project).cache_args)
end end
it 'has a valid function name' do it 'has a valid function name' do
...@@ -88,11 +140,12 @@ describe Projects::Serverless::FunctionsController do ...@@ -88,11 +140,12 @@ describe Projects::Serverless::FunctionsController do
describe 'GET #index with data', :use_clean_rails_memory_store_caching do describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do before do
stub_kubeclient_service_pods 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"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
}) },
*cluster.knative_services_finder(project).cache_args)
end end
it 'has data' do it 'has data' do
...@@ -100,11 +153,16 @@ describe Projects::Serverless::FunctionsController do ...@@ -100,11 +153,16 @@ describe Projects::Serverless::FunctionsController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response).to contain_exactly( expect(json_response).to match(
{
"knative_installed" => "checking",
"functions" => [
a_hash_including( a_hash_including(
"name" => project.name, "name" => project.name,
"url" => "http://#{project.name}.#{namespace.namespace}.example.com" "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
) )
]
}
) )
end end
......
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
describe 'Functions', :js do describe 'Functions', :js do
include KubernetesHelpers include KubernetesHelpers
include ReactiveCachingHelpers
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -13,44 +14,70 @@ describe 'Functions', :js do ...@@ -13,44 +14,70 @@ describe 'Functions', :js do
gitlab_sign_in(user) gitlab_sign_in(user)
end 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 before do
visit project_serverless_functions_path(project) visit project_serverless_functions_path(project)
end 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_link('Install Knative')
expect(page).to have_selector('.empty-state') expect(page).to have_selector('.empty-state')
end end
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 context 'when the user does have a cluster and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
before do it_behaves_like "it's missing knative installation"
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
end end
context 'when the user has a cluster and knative installed and visits the serverless page' do 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(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } let(:project) { cluster.project }
let(:project) { knative.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 before do
stub_kubeclient_knative_services allow_any_instance_of(Clusters::Cluster)
stub_kubeclient_service_pods .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) visit project_serverless_functions_path(project)
end end
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 it 'sees an empty listing of serverless functions' do
expect(page).to have_selector('.empty-state') 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 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 ...@@ -10,7 +10,7 @@ describe Projects::Serverless::FunctionsFinder do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes } let(:service) { cluster.platform_kubernetes }
let(:project) { cluster.project} let(:project) { cluster.project }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
...@@ -23,9 +23,45 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -23,9 +23,45 @@ describe Projects::Serverless::FunctionsFinder do
project.add_maintainer(user) project.add_maintainer(user)
end 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 describe 'retrieve data from knative' do
it 'does not have knative installed' do context 'does not have knative installed' do
expect(described_class.new(project).execute).to be_empty it { expect(described_class.new(project).execute).to be_empty }
end end
context 'has knative installed' do context 'has knative installed' do
...@@ -38,22 +74,24 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -38,22 +74,24 @@ describe Projects::Serverless::FunctionsFinder do
it 'there are functions', :use_clean_rails_memory_store_caching do it 'there are functions', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods 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"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["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 expect(finder.execute).not_to be_empty
end end
it 'has a function', :use_clean_rails_memory_store_caching do it 'has a function', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods 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"], services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["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) result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty expect(result).not_to be_empty
...@@ -84,20 +122,4 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -84,20 +122,4 @@ describe Projects::Serverless::FunctionsFinder do
end end
end 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 end
...@@ -14,7 +14,7 @@ describe('environment row component', () => { ...@@ -14,7 +14,7 @@ describe('environment row component', () => {
beforeEach(() => { beforeEach(() => {
localVue = createLocalVue(); localVue = createLocalVue();
vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*'); vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
}); });
afterEach(() => vm.$destroy()); afterEach(() => vm.$destroy());
...@@ -48,7 +48,11 @@ describe('environment row component', () => { ...@@ -48,7 +48,11 @@ describe('environment row component', () => {
beforeEach(() => { beforeEach(() => {
localVue = createLocalVue(); localVue = createLocalVue();
vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test'); vm = createComponent(
localVue,
translate(mockServerlessFunctionsDiffEnv.functions).test,
'test',
);
}); });
afterEach(() => vm.$destroy()); afterEach(() => vm.$destroy());
......
...@@ -34,11 +34,11 @@ describe('functionsComponent', () => { ...@@ -34,11 +34,11 @@ describe('functionsComponent', () => {
}); });
it('should render empty state when Knative is not installed', () => { it('should render empty state when Knative is not installed', () => {
store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
component = shallowMount(functionsComponent, { component = shallowMount(functionsComponent, {
localVue, localVue,
store, store,
propsData: { propsData: {
installed: false,
clustersPath: '', clustersPath: '',
helpPath: '', helpPath: '',
statusPath: '', statusPath: '',
...@@ -55,7 +55,6 @@ describe('functionsComponent', () => { ...@@ -55,7 +55,6 @@ describe('functionsComponent', () => {
localVue, localVue,
store, store,
propsData: { propsData: {
installed: true,
clustersPath: '', clustersPath: '',
helpPath: '', helpPath: '',
statusPath: '', statusPath: '',
...@@ -67,12 +66,11 @@ describe('functionsComponent', () => { ...@@ -67,12 +66,11 @@ describe('functionsComponent', () => {
}); });
it('should render empty state when there is no function data', () => { it('should render empty state when there is no function data', () => {
store.dispatch('receiveFunctionsNoDataSuccess'); store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
component = shallowMount(functionsComponent, { component = shallowMount(functionsComponent, {
localVue, localVue,
store, store,
propsData: { propsData: {
installed: true,
clustersPath: '', clustersPath: '',
helpPath: '', helpPath: '',
statusPath: '', statusPath: '',
...@@ -91,12 +89,31 @@ describe('functionsComponent', () => { ...@@ -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', () => { it('should render the functions list', () => {
component = shallowMount(functionsComponent, { component = shallowMount(functionsComponent, {
localVue, localVue,
store, store,
propsData: { propsData: {
installed: true,
clustersPath: 'clustersPath', clustersPath: 'clustersPath',
helpPath: 'helpPath', helpPath: 'helpPath',
statusPath, statusPath,
......
export const mockServerlessFunctions = [ export const mockServerlessFunctions = {
knative_installed: true,
functions: [
{ {
name: 'testfunc1', name: 'testfunc1',
namespace: 'tm-example', namespace: 'tm-example',
...@@ -23,9 +25,12 @@ export const mockServerlessFunctions = [ ...@@ -23,9 +25,12 @@ export const mockServerlessFunctions = [
description: 'A second test service\nThis one with additional descriptions', description: 'A second test service\nThis one with additional descriptions',
image: 'knative-test-echo-buildtemplate', image: 'knative-test-echo-buildtemplate',
}, },
]; ],
};
export const mockServerlessFunctionsDiffEnv = [ export const mockServerlessFunctionsDiffEnv = {
knative_installed: true,
functions: [
{ {
name: 'testfunc1', name: 'testfunc1',
namespace: 'tm-example', namespace: 'tm-example',
...@@ -50,7 +55,8 @@ export const mockServerlessFunctionsDiffEnv = [ ...@@ -50,7 +55,8 @@ export const mockServerlessFunctionsDiffEnv = [
description: 'A second test service\nThis one with additional descriptions', description: 'A second test service\nThis one with additional descriptions',
image: 'knative-test-echo-buildtemplate', image: 'knative-test-echo-buildtemplate',
}, },
]; ],
};
export const mockServerlessFunction = { export const mockServerlessFunction = {
name: 'testfunc1', name: 'testfunc1',
......
...@@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => { ...@@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => {
describe('getFunctions', () => { describe('getFunctions', () => {
it('should translate the raw function array to group the functions per environment scope', () => { 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); const funcs = getters.getFunctions(state);
......
...@@ -19,13 +19,13 @@ describe('ServerlessMutations', () => { ...@@ -19,13 +19,13 @@ describe('ServerlessMutations', () => {
expect(state.isLoading).toEqual(false); expect(state.isLoading).toEqual(false);
expect(state.hasFunctionData).toEqual(true); 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', () => { it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
const state = {}; 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.isLoading).toEqual(false);
expect(state.hasFunctionData).toEqual(false); expect(state.hasFunctionData).toEqual(false);
......
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
require 'rails_helper' require 'rails_helper'
describe Clusters::Applications::Knative do describe Clusters::Applications::Knative do
include KubernetesHelpers
include ReactiveCachingHelpers
let(:knative) { create(:clusters_applications_knative) } let(:knative) { create(:clusters_applications_knative) }
include_examples 'cluster application core specs', :clusters_applications_knative include_examples 'cluster application core specs', :clusters_applications_knative
...@@ -146,77 +143,4 @@ describe Clusters::Applications::Knative do ...@@ -146,77 +143,4 @@ describe Clusters::Applications::Knative do
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:hostname) } it { is_expected.to validate_presence_of(:hostname) }
end 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 end
...@@ -38,6 +38,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -38,6 +38,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to respond_to :project } 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 describe '.enabled' do
subject { described_class.enabled } subject { described_class.enabled }
......
...@@ -17,17 +17,38 @@ module KubernetesHelpers ...@@ -17,17 +17,38 @@ module KubernetesHelpers
kube_response(kube_deployments_body) kube_response(kube_deployments_body)
end 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 + '/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
WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body)) .stub_request(:get, api_url + '/apis/extensions/v1beta1')
WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body)) .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 end
def stub_kubeclient_service_pods(status: nil) def stub_kubeclient_service_pods(response = nil, options = {})
stub_kubeclient_discover(service.api_url) 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) WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end end
...@@ -56,15 +77,18 @@ module KubernetesHelpers ...@@ -56,15 +77,18 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response) WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end 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[:name] ||= "kubetest"
options[:namespace] ||= "default"
options[:domain] ||= "example.com" options[:domain] ||= "example.com"
options[:response] ||= kube_response(kube_knative_services_body(options))
stub_kubeclient_discover(service.api_url) 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 end
def stub_kubeclient_get_secret(api_url, **options) 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