diff options
author | Chris Baumbauer <cab@cabnetworks.net> | 2019-04-06 02:02:39 +0000 |
---|---|---|
committer | Mike Greiling <mike@pixelcog.com> | 2019-04-06 02:02:39 +0000 |
commit | b77fe7db3e885edca14c862f362e2bbd43f0e498 (patch) | |
tree | cd984b8bb900a6b3e37c8f6106101ba8617bf524 /spec | |
parent | 8e33e7cf474b61bbc684d993d86cb5aa775a01d0 (diff) | |
download | gitlab-ce-b77fe7db3e885edca14c862f362e2bbd43f0e498.tar.gz |
Add Knative metrics to Prometheus
Diffstat (limited to 'spec')
20 files changed, 775 insertions, 129 deletions
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index 276cf340962..782f5f272d9 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -76,6 +76,15 @@ describe Projects::Serverless::FunctionsController do end end + describe 'GET #metrics' do + context 'invalid data' do + it 'has a bad function name' do + get :metrics, params: params({ format: :json, environment_id: "*", id: "foo" }) + expect(response).to have_gitlab_http_status(204) + end + end + end + describe 'GET #index with data', :use_clean_rails_memory_store_caching do before do stub_kubeclient_service_pods diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb index aa71669de98..e14934b1672 100644 --- a/spec/features/projects/serverless/functions_spec.rb +++ b/spec/features/projects/serverless/functions_spec.rb @@ -50,7 +50,7 @@ describe 'Functions', :js do end it 'sees an empty listing of serverless functions' do - expect(page).to have_selector('.gl-responsive-table-row') + expect(page).to have_selector('.empty-state') end end end diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb index 35279906854..3ad38207da4 100644 --- a/spec/finders/projects/serverless/functions_finder_spec.rb +++ b/spec/finders/projects/serverless/functions_finder_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Projects::Serverless::FunctionsFinder do include KubernetesHelpers + include PrometheusHelpers include ReactiveCachingHelpers let(:user) { create(:user) } @@ -24,12 +25,12 @@ describe Projects::Serverless::FunctionsFinder do describe 'retrieve data from knative' do it 'does not have knative installed' do - expect(described_class.new(project.clusters).execute).to be_empty + expect(described_class.new(project).execute).to be_empty end context 'has knative installed' do let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } - let(:finder) { described_class.new(project.clusters) } + let(:finder) { described_class.new(project) } it 'there are no functions' do expect(finder.execute).to be_empty @@ -58,13 +59,36 @@ describe Projects::Serverless::FunctionsFinder do expect(result).not_to be_empty expect(result["metadata"]["name"]).to be_eql(cluster.project.name) end + + it 'has metrics', :use_clean_rails_memory_store_caching do + end + end + + context 'has prometheus' do + let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } + let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } + let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + let(:finder) { described_class.new(project) } + + before do + allow(finder).to receive(:prometheus_adapter).and_return(prometheus_adapter) + allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix')) + end + + it 'is available' do + expect(finder.has_prometheus?("*")).to be true + end + + it 'has query data' do + expect(finder.invocation_metrics("*", cluster.project.name)).not_to be_nil + 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.clusters).installed?).to be false + expect(described_class.new(project).installed?).to be false end end @@ -72,7 +96,7 @@ describe Projects::Serverless::FunctionsFinder do let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } it 'does have knative installed' do - expect(described_class.new(project.clusters).installed?).to be true + expect(described_class.new(project).installed?).to be true end end end diff --git a/spec/javascripts/serverless/components/area_spec.js b/spec/javascripts/serverless/components/area_spec.js new file mode 100644 index 00000000000..2be6ac3d268 --- /dev/null +++ b/spec/javascripts/serverless/components/area_spec.js @@ -0,0 +1,121 @@ +import { shallowMount } from '@vue/test-utils'; +import Area from '~/serverless/components/area.vue'; +import { mockNormalizedMetrics } from '../mock_data'; + +describe('Area component', () => { + const mockWidgets = 'mockWidgets'; + const mockGraphData = mockNormalizedMetrics; + let areaChart; + + beforeEach(() => { + areaChart = shallowMount(Area, { + propsData: { + graphData: mockGraphData, + containerWidth: 0, + }, + slots: { + default: mockWidgets, + }, + }); + }); + + afterEach(() => { + areaChart.destroy(); + }); + + it('renders chart title', () => { + expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title); + }); + + it('contains graph widgets from slot', () => { + expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + const mockDate = mockNormalizedMetrics.queries[0].result[0].values[0].time; + const generateSeriesData = type => ({ + seriesData: [ + { + componentSubType: type, + value: [mockDate, 4], + }, + ], + value: mockDate, + }); + + describe('series is of line type', () => { + beforeEach(() => { + areaChart.vm.formatTooltipText(generateSeriesData('line')); + }); + + it('formats tooltip title', () => { + expect(areaChart.vm.tooltipPopoverTitle).toBe('28 Feb 2019, 11:11AM'); + }); + + it('formats tooltip content', () => { + expect(areaChart.vm.tooltipPopoverContent).toBe('Invocations (requests): 4'); + }); + }); + + it('verify default interval value of 1', () => { + expect(areaChart.vm.getInterval).toBe(1); + }); + }); + + describe('onResize', () => { + const mockWidth = 233; + + beforeEach(() => { + spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({ + width: mockWidth, + })); + areaChart.vm.onResize(); + }); + + it('sets area chart width', () => { + expect(areaChart.vm.width).toBe(mockWidth); + }); + }); + }); + + describe('computed', () => { + describe('chartData', () => { + it('utilizes all data points', () => { + expect(Object.keys(areaChart.vm.chartData)).toEqual(['requests']); + expect(areaChart.vm.chartData.requests.length).toBe(2); + }); + + it('creates valid data', () => { + const data = areaChart.vm.chartData.requests; + + expect( + data.filter( + datum => new Date(datum.time).getTime() > 0 && typeof datum.value === 'number', + ).length, + ).toBe(data.length); + }); + }); + + describe('generateSeries', () => { + it('utilizes correct time data', () => { + expect(areaChart.vm.generateSeries.data).toEqual([ + ['2019-02-28T11:11:38.756Z', 0], + ['2019-02-28T11:12:38.756Z', 0], + ]); + }); + }); + + describe('xAxisLabel', () => { + it('constructs a label for the chart x-axis', () => { + expect(areaChart.vm.xAxisLabel).toBe('invocations / minute'); + }); + }); + + describe('yAxisLabel', () => { + it('constructs a label for the chart y-axis', () => { + expect(areaChart.vm.yAxisLabel).toBe('Invocations (requests)'); + }); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/javascripts/serverless/components/environment_row_spec.js index bdf7a714910..932d712dbec 100644 --- a/spec/javascripts/serverless/components/environment_row_spec.js +++ b/spec/javascripts/serverless/components/environment_row_spec.js @@ -1,81 +1,70 @@ -import Vue from 'vue'; - import environmentRowComponent from '~/serverless/components/environment_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import ServerlessStore from '~/serverless/stores/serverless_store'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; +import { translate } from '~/serverless/utils'; -const createComponent = (env, envName) => - mountComponent(Vue.extend(environmentRowComponent), { env, envName }); +const createComponent = (localVue, env, envName) => + shallowMount(environmentRowComponent, { localVue, propsData: { env, envName } }).vm; describe('environment row component', () => { describe('default global cluster case', () => { + let localVue; let vm; beforeEach(() => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctions); - vm = createComponent(store.state.functions['*'], '*'); + localVue = createLocalVue(); + vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*'); }); + afterEach(() => vm.$destroy()); + it('has the correct envId', () => { expect(vm.envId).toEqual('env-global'); - vm.$destroy(); }); it('is open by default', () => { expect(vm.isOpenClass).toEqual({ 'is-open': true }); - vm.$destroy(); }); it('generates correct output', () => { - expect(vm.$el.querySelectorAll('li').length).toEqual(2); expect(vm.$el.id).toEqual('env-global'); expect(vm.$el.classList.contains('is-open')).toBe(true); expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*'); - - vm.$destroy(); }); it('opens and closes correctly', () => { expect(vm.isOpen).toBe(true); vm.toggleOpen(); - Vue.nextTick(() => { - expect(vm.isOpen).toBe(false); - }); - vm.$destroy(); + expect(vm.isOpen).toBe(false); }); }); describe('default named cluster case', () => { let vm; + let localVue; beforeEach(() => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv); - vm = createComponent(store.state.functions.test, 'test'); + localVue = createLocalVue(); + vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test'); }); + afterEach(() => vm.$destroy()); + it('has the correct envId', () => { expect(vm.envId).toEqual('env-test'); - vm.$destroy(); }); it('is open by default', () => { expect(vm.isOpenClass).toEqual({ 'is-open': true }); - vm.$destroy(); }); it('generates correct output', () => { - expect(vm.$el.querySelectorAll('li').length).toEqual(1); expect(vm.$el.id).toEqual('env-test'); expect(vm.$el.classList.contains('is-open')).toBe(true); expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test'); - - vm.$destroy(); }); }); }); diff --git a/spec/javascripts/serverless/components/function_details_spec.js b/spec/javascripts/serverless/components/function_details_spec.js new file mode 100644 index 00000000000..a29d4a296ef --- /dev/null +++ b/spec/javascripts/serverless/components/function_details_spec.js @@ -0,0 +1,113 @@ +import Vuex from 'vuex'; + +import functionDetailsComponent from '~/serverless/components/function_details.vue'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createStore } from '~/serverless/store'; + +describe('functionDetailsComponent', () => { + let localVue; + let component; + let store; + + beforeEach(() => { + localVue = createLocalVue(); + localVue.use(Vuex); + + store = createStore(); + }); + + afterEach(() => { + component.vm.$destroy(); + }); + + describe('Verify base functionality', () => { + const serviceStub = { + name: 'test', + description: 'a description', + environment: '*', + url: 'http://service.com/test', + namespace: 'test-ns', + podcount: 0, + metricsUrl: '/metrics', + }; + + it('has a name, description, URL, and no pods loaded', () => { + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + }); + + expect( + component.vm.$el.querySelector('.serverless-function-name').innerHTML.trim(), + ).toContain('test'); + + expect( + component.vm.$el.querySelector('.serverless-function-description').innerHTML.trim(), + ).toContain('a description'); + + expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain( + 'No pods loaded at this time.', + ); + }); + + it('has a pods loaded', () => { + serviceStub.podcount = 1; + + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + }); + + expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('1 pod in use'); + }); + + it('has multiple pods loaded', () => { + serviceStub.podcount = 3; + + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + }); + + expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('3 pods in use'); + }); + + it('can support a missing description', () => { + serviceStub.description = null; + + component = shallowMount(functionDetailsComponent, { + localVue, + store, + propsData: { + func: serviceStub, + hasPrometheus: false, + clustersPath: '/clusters', + helpPath: '/help', + }, + }); + + expect( + component.vm.$el.querySelector('.serverless-function-description').querySelector('div') + .innerHTML.length, + ).toEqual(0); + }); + }); +}); diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/javascripts/serverless/components/function_row_spec.js index 6933a8f6c87..3987e1753bd 100644 --- a/spec/javascripts/serverless/components/function_row_spec.js +++ b/spec/javascripts/serverless/components/function_row_spec.js @@ -1,11 +1,9 @@ -import Vue from 'vue'; - import functionRowComponent from '~/serverless/components/function_row.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import { mockServerlessFunction } from '../mock_data'; -const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func }); +const createComponent = func => shallowMount(functionRowComponent, { propsData: { func } }).vm; describe('functionRowComponent', () => { it('Parses the function details correctly', () => { @@ -13,10 +11,7 @@ describe('functionRowComponent', () => { expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name); expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image); - expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null); - expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual( - mockServerlessFunction.url, - ); + expect(vm.$el.querySelector('timeago-stub').getAttribute('time')).not.toBe(null); vm.$destroy(); }); @@ -25,8 +20,6 @@ describe('functionRowComponent', () => { const vm = createComponent(mockServerlessFunction); expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row - expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image - expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar vm.$destroy(); }); diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js index 85cfe71281f..c32978ea58a 100644 --- a/spec/javascripts/serverless/components/functions_spec.js +++ b/spec/javascripts/serverless/components/functions_spec.js @@ -1,68 +1,101 @@ -import Vue from 'vue'; +import Vuex from 'vuex'; import functionsComponent from '~/serverless/components/functions.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import ServerlessStore from '~/serverless/stores/serverless_store'; - +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createStore } from '~/serverless/store'; import { mockServerlessFunctions } from '../mock_data'; -const createComponent = ( - functions, - installed = true, - loadingData = true, - hasFunctionData = true, -) => { - const component = Vue.extend(functionsComponent); +describe('functionsComponent', () => { + let component; + let store; + let localVue; + + beforeEach(() => { + localVue = createLocalVue(); + localVue.use(Vuex); - return mountComponent(component, { - functions, - installed, - clustersPath: '/testClusterPath', - helpPath: '/helpPath', - loadingData, - hasFunctionData, + store = createStore(); }); -}; -describe('functionsComponent', () => { - it('should render empty state when Knative is not installed', () => { - const vm = createComponent({}, false); + afterEach(() => { + component.vm.$destroy(); + }); - expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true); - expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( - 'Getting started with serverless', - ); + it('should render empty state when Knative is not installed', () => { + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: false, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); - vm.$destroy(); + expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null); }); it('should render a loading component', () => { - const vm = createComponent({}); + store.dispatch('requestFunctionsLoading'); + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: true, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); - expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null); - expect(vm.$el.querySelector('div.animation-container')).not.toBe(null); + expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null); }); it('should render empty state when there is no function data', () => { - const vm = createComponent({}, true, false, false); + store.dispatch('receiveFunctionsNoDataSuccess'); + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: true, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); expect( - vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'), + component.vm.$el + .querySelector('.empty-state, .js-empty-state') + .classList.contains('js-empty-state'), ).toBe(true); - expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual( + expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual( 'No functions available', ); - - vm.$destroy(); }); it('should render the functions list', () => { - const store = new ServerlessStore(false, '/cluster_path', 'help_path'); - store.updateFunctionsFromServer(mockServerlessFunctions); - const vm = createComponent(store.state.functions, true, false); + component = shallowMount(functionsComponent, { + localVue, + store, + propsData: { + installed: true, + clustersPath: '', + helpPath: '', + statusPath: '', + }, + sync: false, + }); + + component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions); - expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null); - expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true); + return component.vm.$nextTick().then(() => { + expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null); + }); }); }); diff --git a/spec/javascripts/serverless/components/missing_prometheus_spec.js b/spec/javascripts/serverless/components/missing_prometheus_spec.js new file mode 100644 index 00000000000..77aca03772b --- /dev/null +++ b/spec/javascripts/serverless/components/missing_prometheus_spec.js @@ -0,0 +1,37 @@ +import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue'; +import { shallowMount } from '@vue/test-utils'; + +const createComponent = missingData => + shallowMount(missingPrometheusComponent, { + propsData: { + clustersPath: '/clusters', + helpPath: '/help', + missingData, + }, + }).vm; + +describe('missingPrometheusComponent', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + it('should render missing prometheus message', () => { + vm = createComponent(false); + + expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( + 'Function invocation metrics require Prometheus to be installed first.', + ); + + expect(vm.$el.querySelector('glbutton-stub').getAttribute('variant')).toEqual('success'); + }); + + it('should render no prometheus data message', () => { + vm = createComponent(true); + + expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain( + 'Invocation metrics loading or not available at this time.', + ); + }); +}); diff --git a/spec/javascripts/serverless/components/pod_box_spec.js b/spec/javascripts/serverless/components/pod_box_spec.js new file mode 100644 index 00000000000..69ac1a2bb5f --- /dev/null +++ b/spec/javascripts/serverless/components/pod_box_spec.js @@ -0,0 +1,22 @@ +import podBoxComponent from '~/serverless/components/pod_box.vue'; +import { shallowMount } from '@vue/test-utils'; + +const createComponent = count => + shallowMount(podBoxComponent, { + propsData: { + count, + }, + }).vm; + +describe('podBoxComponent', () => { + it('should render three boxes', () => { + const count = 3; + const vm = createComponent(count); + const rects = vm.$el.querySelectorAll('rect'); + + expect(rects.length).toEqual(3); + expect(parseInt(rects[2].getAttribute('x'), 10)).toEqual(40); + + vm.$destroy(); + }); +}); diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/javascripts/serverless/components/url_spec.js index 21a879a49bb..08c3e4146b1 100644 --- a/spec/javascripts/serverless/components/url_spec.js +++ b/spec/javascripts/serverless/components/url_spec.js @@ -1,15 +1,13 @@ import Vue from 'vue'; - import urlComponent from '~/serverless/components/url.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -const createComponent = uri => { - const component = Vue.extend(urlComponent); +import { shallowMount } from '@vue/test-utils'; - return mountComponent(component, { - uri, - }); -}; +const createComponent = uri => + shallowMount(Vue.extend(urlComponent), { + propsData: { + uri, + }, + }).vm; describe('urlComponent', () => { it('should render correctly', () => { @@ -17,9 +15,7 @@ describe('urlComponent', () => { const vm = createComponent(uri); expect(vm.$el.classList.contains('clipboard-group')).toBe(true); - expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual( - uri, - ); + expect(vm.$el.querySelector('clipboardbutton-stub').getAttribute('text')).toEqual(uri); expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri); diff --git a/spec/javascripts/serverless/mock_data.js b/spec/javascripts/serverless/mock_data.js index ecd393b174c..a2c18616324 100644 --- a/spec/javascripts/serverless/mock_data.js +++ b/spec/javascripts/serverless/mock_data.js @@ -77,3 +77,60 @@ export const mockMultilineServerlessFunction = { description: 'testfunc1\nA test service line\\nWith additional services', image: 'knative-test-container-buildtemplate', }; + +export const mockMetrics = { + success: true, + last_update: '2019-02-28T19:11:38.926Z', + metrics: { + id: 22, + title: 'Knative function invocations', + required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'], + weight: 0, + y_label: 'Invocations', + queries: [ + { + query_range: + 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))', + unit: 'requests', + label: 'invocations / minute', + result: [ + { + metric: {}, + values: [[1551352298.756, '0'], [1551352358.756, '0']], + }, + ], + }, + ], + }, +}; + +export const mockNormalizedMetrics = { + id: 22, + title: 'Knative function invocations', + required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'], + weight: 0, + y_label: 'Invocations', + queries: [ + { + query_range: + 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))', + unit: 'requests', + label: 'invocations / minute', + result: [ + { + metric: {}, + values: [ + { + time: '2019-02-28T11:11:38.756Z', + value: 0, + }, + { + time: '2019-02-28T11:12:38.756Z', + value: 0, + }, + ], + }, + ], + }, + ], +}; diff --git a/spec/javascripts/serverless/store/actions_spec.js b/spec/javascripts/serverless/store/actions_spec.js new file mode 100644 index 00000000000..602798573e9 --- /dev/null +++ b/spec/javascripts/serverless/store/actions_spec.js @@ -0,0 +1,88 @@ +import MockAdapter from 'axios-mock-adapter'; +import statusCodes from '~/lib/utils/http_status'; +import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions'; +import { mockServerlessFunctions, mockMetrics } from '../mock_data'; +import axios from '~/lib/utils/axios_utils'; +import testAction from '../../helpers/vuex_action_helper'; +import { adjustMetricQuery } from '../utils'; + +describe('ServerlessActions', () => { + describe('fetchFunctions', () => { + it('should successfully fetch functions', done => { + const endpoint = '/functions'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions)); + + testAction( + fetchFunctions, + { functionsPath: endpoint }, + {}, + [], + [ + { type: 'requestFunctionsLoading' }, + { type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions }, + ], + () => { + mock.restore(); + done(); + }, + ); + }); + + it('should successfully retry', done => { + const endpoint = '/functions'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.NO_CONTENT); + + testAction( + fetchFunctions, + { functionsPath: endpoint }, + {}, + [], + [{ type: 'requestFunctionsLoading' }], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('fetchMetrics', () => { + it('should return no prometheus', done => { + const endpoint = '/metrics'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.NO_CONTENT); + + testAction( + fetchMetrics, + { metricsPath: endpoint, hasPrometheus: false }, + {}, + [], + [{ type: 'receiveMetricsNoPrometheus' }], + () => { + mock.restore(); + done(); + }, + ); + }); + + it('should successfully fetch metrics', done => { + const endpoint = '/metrics'; + const mock = new MockAdapter(axios); + mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics)); + + testAction( + fetchMetrics, + { metricsPath: endpoint, hasPrometheus: true }, + {}, + [], + [{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }], + () => { + mock.restore(); + done(); + }, + ); + }); + }); +}); diff --git a/spec/javascripts/serverless/store/getters_spec.js b/spec/javascripts/serverless/store/getters_spec.js new file mode 100644 index 00000000000..fb549c8f153 --- /dev/null +++ b/spec/javascripts/serverless/store/getters_spec.js @@ -0,0 +1,43 @@ +import serverlessState from '~/serverless/store/state'; +import * as getters from '~/serverless/store/getters'; +import { mockServerlessFunctions } from '../mock_data'; + +describe('Serverless Store Getters', () => { + let state; + + beforeEach(() => { + state = serverlessState; + }); + + describe('hasPrometheusMissingData', () => { + it('should return false if Prometheus is not installed', () => { + state.hasPrometheus = false; + + expect(getters.hasPrometheusMissingData(state)).toEqual(false); + }); + + it('should return false if Prometheus is installed and there is data', () => { + state.hasPrometheusData = true; + + expect(getters.hasPrometheusMissingData(state)).toEqual(false); + }); + + it('should return true if Prometheus is installed and there is no data', () => { + state.hasPrometheus = true; + state.hasPrometheusData = false; + + expect(getters.hasPrometheusMissingData(state)).toEqual(true); + }); + }); + + describe('getFunctions', () => { + it('should translate the raw function array to group the functions per environment scope', () => { + state.functions = mockServerlessFunctions; + + const funcs = getters.getFunctions(state); + + expect(Object.keys(funcs)).toContain('*'); + expect(funcs['*'].length).toEqual(2); + }); + }); +}); diff --git a/spec/javascripts/serverless/store/mutations_spec.js b/spec/javascripts/serverless/store/mutations_spec.js new file mode 100644 index 00000000000..ca3053e5c38 --- /dev/null +++ b/spec/javascripts/serverless/store/mutations_spec.js @@ -0,0 +1,86 @@ +import mutations from '~/serverless/store/mutations'; +import * as types from '~/serverless/store/mutation_types'; +import { mockServerlessFunctions, mockMetrics } from '../mock_data'; + +describe('ServerlessMutations', () => { + describe('Functions List Mutations', () => { + it('should ensure loading is true', () => { + const state = {}; + + mutations[types.REQUEST_FUNCTIONS_LOADING](state); + + expect(state.isLoading).toEqual(true); + }); + + it('should set proper state once functions are loaded', () => { + const state = {}; + + mutations[types.RECEIVE_FUNCTIONS_SUCCESS](state, mockServerlessFunctions); + + expect(state.isLoading).toEqual(false); + expect(state.hasFunctionData).toEqual(true); + expect(state.functions).toEqual(mockServerlessFunctions); + }); + + 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); + + expect(state.isLoading).toEqual(false); + expect(state.hasFunctionData).toEqual(false); + expect(state.functions).toBe(undefined); + }); + + it('should ensure loading has stopped, and an error is raised', () => { + const state = {}; + + mutations[types.RECEIVE_FUNCTIONS_ERROR](state, 'sample error'); + + expect(state.isLoading).toEqual(false); + expect(state.hasFunctionData).toEqual(false); + expect(state.functions).toBe(undefined); + expect(state.error).not.toBe(undefined); + }); + }); + + describe('Function Details Metrics Mutations', () => { + it('should ensure isLoading and hasPrometheus data flags indicate data is loaded', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_SUCCESS](state, mockMetrics); + + expect(state.isLoading).toEqual(false); + expect(state.hasPrometheusData).toEqual(true); + expect(state.graphData).toEqual(mockMetrics); + }); + + it('should ensure isLoading and hasPrometheus data flags are cleared indicating no functions available', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_NODATA_SUCCESS](state); + + expect(state.isLoading).toEqual(false); + expect(state.hasPrometheusData).toEqual(false); + expect(state.graphData).toBe(undefined); + }); + + it('should properly indicate an error', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_ERROR](state, 'sample error'); + + expect(state.hasPrometheusData).toEqual(false); + expect(state.error).not.toBe(undefined); + }); + + it('should properly indicate when prometheus is installed', () => { + const state = {}; + + mutations[types.RECEIVE_METRICS_NO_PROMETHEUS](state); + + expect(state.hasPrometheus).toEqual(false); + expect(state.hasPrometheusData).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js deleted file mode 100644 index 72fd903d7d1..00000000000 --- a/spec/javascripts/serverless/stores/serverless_store_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import ServerlessStore from '~/serverless/stores/serverless_store'; -import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data'; - -describe('Serverless Functions Store', () => { - let store; - - beforeEach(() => { - store = new ServerlessStore(false, '/cluster_path', 'help_path'); - }); - - describe('#updateFunctionsFromServer', () => { - it('should pass an empty hash object', () => { - store.updateFunctionsFromServer(); - - expect(store.state.functions).toEqual({}); - }); - - it('should group functions to one global environment', () => { - const mockServerlessData = mockServerlessFunctions; - store.updateFunctionsFromServer(mockServerlessData); - - expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); - expect(store.state.functions['*'].length).toEqual(2); - }); - - it('should group functions to multiple environments', () => { - const mockServerlessData = mockServerlessFunctionsDiffEnv; - store.updateFunctionsFromServer(mockServerlessData); - - expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*'])); - expect(store.state.functions['*'].length).toEqual(1); - expect(store.state.functions.test.length).toEqual(1); - expect(store.state.functions.test[0].name).toEqual('testfunc2'); - }); - }); -}); diff --git a/spec/javascripts/serverless/utils.js b/spec/javascripts/serverless/utils.js new file mode 100644 index 00000000000..5ce2e37d493 --- /dev/null +++ b/spec/javascripts/serverless/utils.js @@ -0,0 +1,20 @@ +export const adjustMetricQuery = data => { + const updatedMetric = data.metrics; + + const queries = data.metrics.queries.map(query => ({ + ...query, + result: query.result.map(result => ({ + ...result, + values: result.values.map(([timestamp, value]) => ({ + time: new Date(timestamp * 1000).toISOString(), + value: Number(value), + })), + })), + })); + + updatedMetric.queries = queries; + return updatedMetric; +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb new file mode 100644 index 00000000000..7f6283715f2 --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do + include PrometheusHelpers + + let(:project) { create(:project) } + let(:serverless_func) { Serverless::Function.new(project, 'test-name', 'test-ns') } + + let(:client) { double('prometheus_client') } + subject { described_class.new(client) } + + context 'verify queries' do + before do + allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns'))) + allow(client).to receive(:query_range) + end + + it 'has the query, but no data' do + results = subject.query(serverless_func.id) + + expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))') + end + end +end diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb new file mode 100644 index 00000000000..1854d5f9415 --- /dev/null +++ b/spec/models/serverless/function_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Serverless::Function do + let(:project) { create(:project) } + let(:func) { described_class.new(project, 'test', 'test-ns') } + + it 'has a proper id' do + expect(func.id).to eql("#{project.id}/test/test-ns") + expect(func.name).to eql("test") + expect(func.namespace).to eql("test-ns") + end + + it 'can decode an identifier' do + f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns") + + expect(f.name).to eql("testfunc") + expect(f.namespace).to eql("dummy-ns") + end +end diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index 08d1d7a6059..87f825152cf 100644 --- a/spec/support/helpers/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb @@ -7,6 +7,10 @@ module PrometheusHelpers %{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100} end + def prometheus_istio_query(function_name, kube_namespace) + %{floor(sum(rate(istio_revision_request_count{destination_configuration=\"#{function_name}\", destination_namespace=\"#{kube_namespace}\"}[1m])*30))} + end + def prometheus_ping_url(prometheus_query) query = { query: prometheus_query }.to_query |