Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
gitlab-ce
Commits
3198867f
Commit
3198867f
authored
Apr 06, 2019
by
Mike Greiling
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'knative-prometheus' into 'master'
Add Knative metrics to Prometheus See merge request gitlab-org/gitlab-ce!24663
parents
8e33e7cf
b77fe7db
Changes
52
Show whitespace changes
Inline
Side-by-side
Showing
52 changed files
with
1516 additions
and
320 deletions
+1516
-320
app/assets/javascripts/serverless/components/area.vue
app/assets/javascripts/serverless/components/area.vue
+146
-0
app/assets/javascripts/serverless/components/function_details.vue
...ts/javascripts/serverless/components/function_details.vue
+50
-4
app/assets/javascripts/serverless/components/function_row.vue
...assets/javascripts/serverless/components/function_row.vue
+5
-0
app/assets/javascripts/serverless/components/functions.vue
app/assets/javascripts/serverless/components/functions.vue
+24
-20
app/assets/javascripts/serverless/components/missing_prometheus.vue
.../javascripts/serverless/components/missing_prometheus.vue
+63
-0
app/assets/javascripts/serverless/constants.js
app/assets/javascripts/serverless/constants.js
+3
-0
app/assets/javascripts/serverless/serverless_bundle.js
app/assets/javascripts/serverless/serverless_bundle.js
+26
-99
app/assets/javascripts/serverless/services/get_functions_service.js
.../javascripts/serverless/services/get_functions_service.js
+0
-11
app/assets/javascripts/serverless/store/actions.js
app/assets/javascripts/serverless/store/actions.js
+113
-0
app/assets/javascripts/serverless/store/getters.js
app/assets/javascripts/serverless/store/getters.js
+10
-0
app/assets/javascripts/serverless/store/index.js
app/assets/javascripts/serverless/store/index.js
+18
-0
app/assets/javascripts/serverless/store/mutation_types.js
app/assets/javascripts/serverless/store/mutation_types.js
+9
-0
app/assets/javascripts/serverless/store/mutations.js
app/assets/javascripts/serverless/store/mutations.js
+38
-0
app/assets/javascripts/serverless/store/state.js
app/assets/javascripts/serverless/store/state.js
+13
-0
app/assets/javascripts/serverless/stores/serverless_details_store.js
...javascripts/serverless/stores/serverless_details_store.js
+0
-11
app/assets/javascripts/serverless/stores/serverless_store.js
app/assets/javascripts/serverless/stores/serverless_store.js
+0
-29
app/assets/javascripts/serverless/utils.js
app/assets/javascripts/serverless/utils.js
+23
-0
app/controllers/projects/serverless/functions_controller.rb
app/controllers/projects/serverless/functions_controller.rb
+17
-6
app/finders/projects/serverless/functions_finder.rb
app/finders/projects/serverless/functions_finder.rb
+26
-2
app/models/serverless/function.rb
app/models/serverless/function.rb
+26
-0
app/serializers/projects/serverless/service_entity.rb
app/serializers/projects/serverless/service_entity.rb
+7
-0
app/views/projects/serverless/functions/index.html.haml
app/views/projects/serverless/functions/index.html.haml
+4
-1
app/views/projects/serverless/functions/show.html.haml
app/views/projects/serverless/functions/show.html.haml
+8
-3
changelogs/unreleased/knative-prometheus.yml
changelogs/unreleased/knative-prometheus.yml
+5
-0
config/prometheus/common_metrics.yml
config/prometheus/common_metrics.yml
+10
-0
config/routes/project.rb
config/routes/project.rb
+5
-1
db/migrate/20190326164045_import_common_metrics_knative.rb
db/migrate/20190326164045_import_common_metrics_knative.rb
+17
-0
db/schema.rb
db/schema.rb
+1
-1
doc/user/project/clusters/serverless/img/function-details-loaded.png
...oject/clusters/serverless/img/function-details-loaded.png
+0
-0
doc/user/project/clusters/serverless/index.md
doc/user/project/clusters/serverless/index.md
+20
-0
lib/gitlab/prometheus/queries/knative_invocation_query.rb
lib/gitlab/prometheus/queries/knative_invocation_query.rb
+39
-0
locale/gitlab.pot
locale/gitlab.pot
+15
-3
spec/controllers/projects/serverless/functions_controller_spec.rb
...trollers/projects/serverless/functions_controller_spec.rb
+9
-0
spec/features/projects/serverless/functions_spec.rb
spec/features/projects/serverless/functions_spec.rb
+1
-1
spec/finders/projects/serverless/functions_finder_spec.rb
spec/finders/projects/serverless/functions_finder_spec.rb
+28
-4
spec/javascripts/serverless/components/area_spec.js
spec/javascripts/serverless/components/area_spec.js
+121
-0
spec/javascripts/serverless/components/environment_row_spec.js
...javascripts/serverless/components/environment_row_spec.js
+15
-26
spec/javascripts/serverless/components/function_details_spec.js
...avascripts/serverless/components/function_details_spec.js
+113
-0
spec/javascripts/serverless/components/function_row_spec.js
spec/javascripts/serverless/components/function_row_spec.js
+3
-10
spec/javascripts/serverless/components/functions_spec.js
spec/javascripts/serverless/components/functions_spec.js
+73
-40
spec/javascripts/serverless/components/missing_prometheus_spec.js
...ascripts/serverless/components/missing_prometheus_spec.js
+37
-0
spec/javascripts/serverless/components/pod_box_spec.js
spec/javascripts/serverless/components/pod_box_spec.js
+22
-0
spec/javascripts/serverless/components/url_spec.js
spec/javascripts/serverless/components/url_spec.js
+8
-12
spec/javascripts/serverless/mock_data.js
spec/javascripts/serverless/mock_data.js
+57
-0
spec/javascripts/serverless/store/actions_spec.js
spec/javascripts/serverless/store/actions_spec.js
+88
-0
spec/javascripts/serverless/store/getters_spec.js
spec/javascripts/serverless/store/getters_spec.js
+43
-0
spec/javascripts/serverless/store/mutations_spec.js
spec/javascripts/serverless/store/mutations_spec.js
+86
-0
spec/javascripts/serverless/stores/serverless_store_spec.js
spec/javascripts/serverless/stores/serverless_store_spec.js
+0
-36
spec/javascripts/serverless/utils.js
spec/javascripts/serverless/utils.js
+20
-0
spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
...itlab/prometheus/queries/knative_invocation_query_spec.rb
+26
-0
spec/models/serverless/function_spec.rb
spec/models/serverless/function_spec.rb
+21
-0
spec/support/helpers/prometheus_helpers.rb
spec/support/helpers/prometheus_helpers.rb
+4
-0
No files found.
app/assets/javascripts/serverless/components/area.vue
0 → 100644
View file @
3198867f
<
script
>
import
{
GlAreaChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
{
debounceByAnimationFrame
}
from
'
~/lib/utils/common_utils
'
;
import
dateFormat
from
'
dateformat
'
;
import
{
X_INTERVAL
}
from
'
../constants
'
;
import
{
validateGraphData
}
from
'
../utils
'
;
let
debouncedResize
;
export
default
{
components
:
{
GlAreaChart
,
},
inheritAttrs
:
false
,
props
:
{
graphData
:
{
type
:
Object
,
required
:
true
,
validator
:
validateGraphData
,
},
containerWidth
:
{
type
:
Number
,
required
:
true
,
},
},
data
()
{
return
{
tooltipPopoverTitle
:
''
,
tooltipPopoverContent
:
''
,
width
:
this
.
containerWidth
,
};
},
computed
:
{
chartData
()
{
return
this
.
graphData
.
queries
.
reduce
((
accumulator
,
query
)
=>
{
accumulator
[
query
.
unit
]
=
query
.
result
.
reduce
((
acc
,
res
)
=>
acc
.
concat
(
res
.
values
),
[]);
return
accumulator
;
},
{});
},
extractTimeData
()
{
return
this
.
chartData
.
requests
.
map
(
data
=>
data
.
time
);
},
generateSeries
()
{
return
{
name
:
'
Invocations
'
,
type
:
'
line
'
,
data
:
this
.
chartData
.
requests
.
map
(
data
=>
[
data
.
time
,
data
.
value
]),
symbolSize
:
0
,
};
},
getInterval
()
{
const
{
result
}
=
this
.
graphData
.
queries
[
0
];
if
(
result
.
length
===
0
)
{
return
1
;
}
const
split
=
result
[
0
].
values
.
reduce
(
(
acc
,
pair
)
=>
(
pair
.
value
>
acc
?
pair
.
value
:
acc
),
1
,
);
return
split
<
X_INTERVAL
?
split
:
X_INTERVAL
;
},
chartOptions
()
{
return
{
xAxis
:
{
name
:
'
time
'
,
type
:
'
time
'
,
axisLabel
:
{
formatter
:
date
=>
dateFormat
(
date
,
'
h:MM TT
'
),
},
data
:
this
.
extractTimeData
,
nameTextStyle
:
{
padding
:
[
18
,
0
,
0
,
0
],
},
},
yAxis
:
{
name
:
this
.
yAxisLabel
,
nameTextStyle
:
{
padding
:
[
0
,
0
,
36
,
0
],
},
splitNumber
:
this
.
getInterval
,
},
legend
:
{
formatter
:
this
.
xAxisLabel
,
},
series
:
this
.
generateSeries
,
};
},
xAxisLabel
()
{
return
this
.
graphData
.
queries
.
map
(
query
=>
query
.
label
).
join
(
'
,
'
);
},
yAxisLabel
()
{
const
[
query
]
=
this
.
graphData
.
queries
;
return
`
${
this
.
graphData
.
y_label
}
(
${
query
.
unit
}
)`
;
},
},
watch
:
{
containerWidth
:
'
onResize
'
,
},
beforeDestroy
()
{
window
.
removeEventListener
(
'
resize
'
,
debouncedResize
);
},
created
()
{
debouncedResize
=
debounceByAnimationFrame
(
this
.
onResize
);
window
.
addEventListener
(
'
resize
'
,
debouncedResize
);
},
methods
:
{
formatTooltipText
(
params
)
{
const
[
seriesData
]
=
params
.
seriesData
;
this
.
tooltipPopoverTitle
=
dateFormat
(
params
.
value
,
'
dd mmm yyyy, h:MMTT
'
);
this
.
tooltipPopoverContent
=
`
${
this
.
yAxisLabel
}
:
${
seriesData
.
value
[
1
]}
`
;
},
onResize
()
{
const
{
width
}
=
this
.
$refs
.
areaChart
.
$el
.
getBoundingClientRect
();
this
.
width
=
width
;
},
},
};
</
script
>
<
template
>
<div
class=
"prometheus-graph"
>
<div
class=
"prometheus-graph-header"
>
<h5
ref=
"graphTitle"
class=
"prometheus-graph-title"
>
{{
graphData
.
title
}}
</h5>
<div
ref=
"graphWidgets"
class=
"prometheus-graph-widgets"
><slot></slot></div>
</div>
<gl-area-chart
ref=
"areaChart"
v-bind=
"$attrs"
:data=
"[]"
:option=
"chartOptions"
:format-tooltip-text=
"formatTooltipText"
:width=
"width"
:include-legend-avg-max=
"false"
>
<template
slot=
"tooltipTitle"
>
{{
tooltipPopoverTitle
}}
</
template
>
<
template
slot=
"tooltipContent"
>
{{
tooltipPopoverContent
}}
</
template
>
</gl-area-chart>
</div>
</template>
app/assets/javascripts/serverless/components/function_details.vue
View file @
3198867f
<
script
>
import
_
from
'
underscore
'
;
import
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
PodBox
from
'
./pod_box.vue
'
;
import
Url
from
'
./url.vue
'
;
import
AreaChart
from
'
./area.vue
'
;
import
MissingPrometheus
from
'
./missing_prometheus.vue
'
;
export
default
{
components
:
{
PodBox
,
Url
,
AreaChart
,
MissingPrometheus
,
},
props
:
{
func
:
{
type
:
Object
,
required
:
true
,
},
hasPrometheus
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
clustersPath
:
{
type
:
String
,
required
:
true
,
},
helpPath
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
elWidth
:
0
,
};
},
computed
:
{
name
()
{
return
this
.
func
.
name
;
},
description
()
{
return
this
.
func
.
description
;
return
_
.
isString
(
this
.
func
.
description
)
?
this
.
func
.
description
:
''
;
},
funcUrl
()
{
return
this
.
func
.
url
;
},
podCount
()
{
return
this
.
func
.
podcount
||
0
;
return
Number
(
this
.
func
.
podcount
)
||
0
;
},
...
mapState
([
'
graphData
'
,
'
hasPrometheusData
'
]),
...
mapGetters
([
'
hasPrometheusMissingData
'
]),
},
created
()
{
this
.
fetchMetrics
({
metricsPath
:
this
.
func
.
metricsUrl
,
hasPrometheus
:
this
.
hasPrometheus
,
});
},
mounted
()
{
this
.
elWidth
=
this
.
$el
.
clientWidth
;
},
methods
:
{
...
mapActions
([
'
fetchMetrics
'
]),
},
};
</
script
>
<
template
>
<section
id=
"serverless-function-details"
>
<h3>
{{
name
}}
</h3>
<div
class=
"append-bottom-default"
>
<h3
class=
"serverless-function-name"
>
{{
name
}}
</h3>
<div
class=
"append-bottom-default
serverless-function-description
"
>
<div
v-for=
"(line, index) in description.split('\n')"
:key=
"index"
>
{{
line
}}
</div>
</div>
<url
:uri=
"funcUrl"
/>
...
...
@@ -52,5 +90,13 @@ export default {
</p>
</div>
<div
v-else
><p>
No pods loaded at this time.
</p></div>
<area-chart
v-if=
"hasPrometheusData"
:graph-data=
"graphData"
:container-width=
"elWidth"
/>
<missing-prometheus
v-if=
"!hasPrometheus || hasPrometheusMissingData"
:help-path=
"helpPath"
:clusters-path=
"clustersPath"
:missing-data=
"hasPrometheusMissingData"
/>
</section>
</
template
>
app/assets/javascripts/serverless/components/function_row.vue
View file @
3198867f
<
script
>
import
_
from
'
underscore
'
;
import
Timeago
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
Url
from
'
./url.vue
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
...
...
@@ -19,6 +20,10 @@ export default {
return
this
.
func
.
name
;
},
description
()
{
if
(
!
_
.
isString
(
this
.
func
.
description
))
{
return
''
;
}
const
desc
=
this
.
func
.
description
.
split
(
'
\n
'
);
if
(
desc
.
length
>
1
)
{
return
desc
[
1
];
...
...
app/assets/javascripts/serverless/components/functions.vue
View file @
3198867f
<
script
>
import
{
GlSkeletonLoading
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
FunctionRow
from
'
./function_row.vue
'
;
import
EnvironmentRow
from
'
./environment_row.vue
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
...
...
@@ -9,14 +10,9 @@ export default {
EnvironmentRow
,
FunctionRow
,
EmptyState
,
Gl
SkeletonLoading
,
Gl
LoadingIcon
,
},
props
:
{
functions
:
{
type
:
Object
,
required
:
true
,
default
:
()
=>
({}),
},
installed
:
{
type
:
Boolean
,
required
:
true
,
...
...
@@ -29,16 +25,22 @@ export default {
type
:
String
,
required
:
true
,
},
loadingData
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
statusPath
:
{
type
:
String
,
required
:
true
,
},
hasFunctionData
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
computed
:
{
...
mapState
([
'
isLoading
'
,
'
hasFunctionData
'
]),
...
mapGetters
([
'
getFunctions
'
]),
},
created
()
{
this
.
fetchFunctions
({
functionsPath
:
this
.
statusPath
,
});
},
methods
:
{
...
mapActions
([
'
fetchFunctions
'
]),
},
};
</
script
>
...
...
@@ -47,14 +49,16 @@ export default {
<section
id=
"serverless-functions"
>
<div
v-if=
"installed"
>
<div
v-if=
"hasFunctionData"
>
<template
v-if=
"loadingData"
>
<div
v-for=
"j in 3"
:key=
"j"
class=
"gl-responsive-table-row"
><gl-skeleton-loading
/></div>
</
template
>
<gl-loading-icon
v-if=
"isLoading"
:size=
"2"
class=
"prepend-top-default append-bottom-default"
/>
<template
v-else
>
<div
class=
"groups-list-tree-container"
>
<ul
class=
"content-list group-list-tree"
>
<environment-row
v-for=
"(env, index) in
f
unctions"
v-for=
"(env, index) in
getF
unctions"
:key=
"index"
:env=
"env"
:env-name=
"index"
...
...
app/assets/javascripts/serverless/components/missing_prometheus.vue
0 → 100644
View file @
3198867f
<
script
>
import
{
GlButton
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
../../locale
'
;
export
default
{
components
:
{
GlButton
,
GlLink
,
},
props
:
{
clustersPath
:
{
type
:
String
,
required
:
true
,
},
helpPath
:
{
type
:
String
,
required
:
true
,
},
missingData
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
missingStateClass
()
{
return
this
.
missingData
?
'
missing-prometheus-state
'
:
'
empty-prometheus-state
'
;
},
prometheusHelpPath
()
{
return
`
${
this
.
helpPath
}
#prometheus-support`
;
},
description
()
{
return
this
.
missingData
?
s__
(
`ServerlessDetails|Invocation metrics loading or not available at this time.`
)
:
s__
(
`ServerlessDetails|Function invocation metrics require Prometheus to be installed first.`
,
);
},
},
};
</
script
>
<
template
>
<div
class=
"row"
:class=
"missingStateClass"
>
<div
class=
"col-12"
>
<div
class=
"text-content"
>
<h4
class=
"state-title text-left"
>
{{
s__
(
`ServerlessDetails|Invocations`
)
}}
</h4>
<p
class=
"state-description"
>
{{
description
}}
<gl-link
:href=
"prometheusHelpPath"
>
{{
s__
(
`ServerlessDetails|More information`
)
}}
</gl-link
>
.
</p>
<div
v-if=
"!missingData"
class=
"text-left"
>
<gl-button
:href=
"clustersPath"
variant=
"success"
>
{{
s__
(
'
ServerlessDetails|Install Prometheus
'
)
}}
</gl-button>
</div>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/serverless/constants.js
0 → 100644
View file @
3198867f
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
app/assets/javascripts/serverless/serverless_bundle.js
View file @
3198867f
import
Visibility
from
'
visibilityjs
'
;
import
Vue
from
'
vue
'
;
import
{
s__
}
from
'
../locale
'
;
import
Flash
from
'
../flash
'
;
import
Poll
from
'
../lib/utils/poll
'
;
import
ServerlessStore
from
'
./stores/serverless_store
'
;
import
ServerlessDetailsStore
from
'
./stores/serverless_details_store
'
;
import
GetFunctionsService
from
'
./services/get_functions_service
'
;
import
Functions
from
'
./components/functions.vue
'
;
import
FunctionDetails
from
'
./components/function_details.vue
'
;
import
{
createStore
}
from
'
./store
'
;
export
default
class
Serverless
{
constructor
()
{
...
...
@@ -19,10 +13,12 @@ export default class Serverless {
serviceUrl
,
serviceNamespace
,
servicePodcount
,
serviceMetricsUrl
,
prometheus
,
clustersPath
,
helpPath
,
}
=
document
.
querySelector
(
'
.js-serverless-function-details-page
'
).
dataset
;
const
el
=
document
.
querySelector
(
'
#js-serverless-function-details
'
);
this
.
store
=
new
ServerlessDetailsStore
();
const
{
store
}
=
this
;
const
service
=
{
name
:
serviceName
,
...
...
@@ -31,20 +27,19 @@ export default class Serverless {
url
:
serviceUrl
,
namespace
:
serviceNamespace
,
podcount
:
servicePodcount
,
metricsUrl
:
serviceMetricsUrl
,
};
this
.
store
.
updateDetailedFunction
(
service
);
this
.
functionDetails
=
new
Vue
({
el
,
data
()
{
return
{
state
:
store
.
state
,
};
},
store
:
createStore
(),
render
(
createElement
)
{
return
createElement
(
FunctionDetails
,
{
props
:
{
func
:
this
.
state
.
functionDetail
,
func
:
service
,
hasPrometheus
:
prometheus
!==
undefined
,
clustersPath
,
helpPath
,
},
});
},
...
...
@@ -54,95 +49,27 @@ export default class Serverless {
'
.js-serverless-functions-page
'
,
).
dataset
;
this
.
service
=
new
GetFunctionsService
(
statusPath
);
this
.
knativeInstalled
=
installed
!==
undefined
;
this
.
store
=
new
ServerlessStore
(
this
.
knativeInstalled
,
clustersPath
,
helpPath
);
this
.
initServerless
();
this
.
functionLoadCount
=
0
;
if
(
statusPath
&&
this
.
knativeInstalled
)
{
this
.
initPolling
();
}
}
}
initServerless
()
{
const
{
store
}
=
this
;
const
el
=
document
.
querySelector
(
'
#js-serverless-functions
'
);
this
.
functions
=
new
Vue
({
el
,
data
()
{
return
{
state
:
store
.
state
,
};
},
store
:
createStore
(),
render
(
createElement
)
{
return
createElement
(
Functions
,
{
props
:
{
functions
:
this
.
state
.
functions
,
installed
:
this
.
state
.
installed
,
clustersPath
:
this
.
state
.
clustersPath
,
helpPath
:
this
.
state
.
helpPath
,
loadingData
:
this
.
state
.
loadingData
,
hasFunctionData
:
this
.
state
.
hasFunctionData
,
installed
:
installed
!==
undefined
,
clustersPath
,
helpPath
,
statusPath
,
},
});
},
});
}
initPolling
()
{
this
.
poll
=
new
Poll
({
resource
:
this
.
service
,
method
:
'
fetchData
'
,
successCallback
:
data
=>
this
.
handleSuccess
(
data
),
errorCallback
:
()
=>
Serverless
.
handleError
(),
});
if
(
!
Visibility
.
hidden
())
{
this
.
poll
.
makeRequest
();
}
else
{
this
.
service
.
fetchData
()
.
then
(
data
=>
this
.
handleSuccess
(
data
))
.
catch
(()
=>
Serverless
.
handleError
());
}
Visibility
.
change
(()
=>
{
if
(
!
Visibility
.
hidden
()
&&
!
this
.
destroyed
)
{
this
.
poll
.
restart
();
}
else
{
this
.
poll
.
stop
();
}
});
}
handleSuccess
(
data
)
{
if
(
data
.
status
===
200
)
{
this
.
store
.
updateFunctionsFromServer
(
data
.
data
);
this
.
store
.
updateLoadingState
(
false
);
}
else
if
(
data
.
status
===
204
)
{
/* Time out after 3 attempts to retrieve data */
this
.
functionLoadCount
+=
1
;
if
(
this
.
functionLoadCount
===
3
)
{
this
.
poll
.
stop
();
this
.
store
.
toggleNoFunctionData
();
}
}
}
static
handleError
()
{
Flash
(
s__
(
'
Serverless|An error occurred while retrieving serverless components
'
));
}
destroy
()
{
this
.
destroyed
=
true
;
if
(
this
.
poll
)
{
this
.
poll
.
stop
();
}
this
.
functions
.
$destroy
();
this
.
functionDetails
.
$destroy
();
}
...
...
app/assets/javascripts/serverless/services/get_functions_service.js
deleted
100644 → 0
View file @
8e33e7cf
import
axios
from
'
~/lib/utils/axios_utils
'
;
export
default
class
GetFunctionsService
{
constructor
(
endpoint
)
{
this
.
endpoint
=
endpoint
;
}
fetchData
()
{
return
axios
.
get
(
this
.
endpoint
);
}
}
app/assets/javascripts/serverless/store/actions.js
0 → 100644
View file @
3198867f
import
*
as
types
from
'
./mutation_types
'
;
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
'
;
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
receiveFunctionsError
=
({
commit
},
error
)
=>
commit
(
types
.
RECEIVE_FUNCTIONS_ERROR
,
error
);
export
const
receiveMetricsSuccess
=
({
commit
},
data
)
=>
commit
(
types
.
RECEIVE_METRICS_SUCCESS
,
data
);
export
const
receiveMetricsNoPrometheus
=
({
commit
})
=>
commit
(
types
.
RECEIVE_METRICS_NO_PROMETHEUS
);
export
const
receiveMetricsNoDataSuccess
=
({
commit
},
data
)
=>
commit
(
types
.
RECEIVE_METRICS_NODATA_SUCCESS
,
data
);
export
const
receiveMetricsError
=
({
commit
},
error
)
=>
commit
(
types
.
RECEIVE_METRICS_ERROR
,
error
);
export
const
fetchFunctions
=
({
dispatch
},
{
functionsPath
})
=>
{
let
retryCount
=
0
;
dispatch
(
'
requestFunctionsLoading
'
);
backOff
((
next
,
stop
)
=>
{
axios
.
get
(
functionsPath
)
.
then
(
response
=>
{
if
(
response
.
status
===
statusCodes
.
NO_CONTENT
)
{
retryCount
+=
1
;
if
(
retryCount
<
MAX_REQUESTS
)
{
next
();
}
else
{
stop
(
null
);
}
}
else
{
stop
(
response
.
data
);
}
})
.
catch
(
stop
);
})
.
then
(
data
=>
{
if
(
data
!==
null
)
{
dispatch
(
'
receiveFunctionsSuccess
'
,
data
);
}
else
{
dispatch
(
'
receiveFunctionsNoDataSuccess
'
);
}
})
.
catch
(
error
=>
{
dispatch
(
'
receiveFunctionsError
'
,
error
);
createFlash
(
error
);
});
};
export
const
fetchMetrics
=
({
dispatch
},
{
metricsPath
,
hasPrometheus
})
=>
{
let
retryCount
=
0
;
if
(
!
hasPrometheus
)
{
dispatch
(
'
receiveMetricsNoPrometheus
'
);
return
;
}
backOff
((
next
,
stop
)
=>
{
axios
.
get
(
metricsPath
)
.
then
(
response
=>
{
if
(
response
.
status
===
statusCodes
.
NO_CONTENT
)
{
retryCount
+=
1
;
if
(
retryCount
<
MAX_REQUESTS
)
{
next
();
}
else
{
dispatch
(
'
receiveMetricsNoDataSuccess
'
);
stop
(
null
);
}
}
else
{
stop
(
response
.
data
);
}
})
.
catch
(
stop
);
})
.
then
(
data
=>
{
if
(
data
===
null
)
{
return
;
}
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
;
dispatch
(
'
receiveMetricsSuccess
'
,
updatedMetric
);
})
.
catch
(
error
=>
{
dispatch
(
'
receiveMetricsError
'
,
error
);
createFlash
(
error
);
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/serverless/store/getters.js
0 → 100644
View file @
3198867f
import
{
translate
}
from
'
../utils
'
;
export
const
hasPrometheusMissingData
=
state
=>
state
.
hasPrometheus
&&
!
state
.
hasPrometheusData
;
// Convert the function list into a k/v grouping based on the environment scope
export
const
getFunctions
=
state
=>
translate
(
state
.
functions
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/serverless/store/index.js
0 → 100644
View file @
3198867f
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
*
as
actions
from
'
./actions
'
;
import
*
as
getters
from
'
./getters
'
;
import
mutations
from
'
./mutations
'
;
import
createState
from
'
./state
'
;
Vue
.
use
(
Vuex
);
export
const
createStore
=
()
=>
new
Vuex
.
Store
({
actions
,
getters
,
mutations
,
state
:
createState
(),
});
export
default
createStore
();
app/assets/javascripts/serverless/store/mutation_types.js
0 → 100644
View file @
3198867f
export
const
REQUEST_FUNCTIONS_LOADING
=
'
REQUEST_FUNCTIONS_LOADING
'
;
export
const
RECEIVE_FUNCTIONS_SUCCESS
=
'
RECEIVE_FUNCTIONS_SUCCESS
'
;
export
const
RECEIVE_FUNCTIONS_NODATA_SUCCESS
=
'
RECEIVE_FUNCTIONS_NODATA_SUCCESS
'
;
export
const
RECEIVE_FUNCTIONS_ERROR
=
'
RECEIVE_FUNCTIONS_ERROR
'
;
export
const
RECEIVE_METRICS_NO_PROMETHEUS
=
'
RECEIVE_METRICS_NO_PROMETHEUS
'
;
export
const
RECEIVE_METRICS_SUCCESS
=
'
RECEIVE_METRICS_SUCCESS
'
;
export
const
RECEIVE_METRICS_NODATA_SUCCESS
=
'
RECEIVE_METRICS_NODATA_SUCCESS
'
;
export
const
RECEIVE_METRICS_ERROR
=
'
RECEIVE_METRICS_ERROR
'
;
app/assets/javascripts/serverless/store/mutations.js
0 → 100644
View file @
3198867f
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
[
types
.
REQUEST_FUNCTIONS_LOADING
](
state
)
{
state
.
isLoading
=
true
;
},
[
types
.
RECEIVE_FUNCTIONS_SUCCESS
](
state
,
data
)
{
state
.
functions
=
data
;
state
.
isLoading
=
false
;
state
.
hasFunctionData
=
true
;
},
[
types
.
RECEIVE_FUNCTIONS_NODATA_SUCCESS
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasFunctionData
=
false
;
},
[
types
.
RECEIVE_FUNCTIONS_ERROR
](
state
,
error
)
{
state
.
error
=
error
;
state
.
hasFunctionData
=
false
;
state
.
isLoading
=
false
;
},
[
types
.
RECEIVE_METRICS_SUCCESS
](
state
,
data
)
{
state
.
isLoading
=
false
;
state
.
hasPrometheusData
=
true
;
state
.
graphData
=
data
;
},
[
types
.
RECEIVE_METRICS_NODATA_SUCCESS
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasPrometheusData
=
false
;
},
[
types
.
RECEIVE_METRICS_ERROR
](
state
,
error
)
{
state
.
hasPrometheusData
=
false
;
state
.
error
=
error
;
},
[
types
.
RECEIVE_METRICS_NO_PROMETHEUS
](
state
)
{
state
.
hasPrometheusData
=
false
;
state
.
hasPrometheus
=
false
;
},
};
app/assets/javascripts/serverless/store/state.js
0 → 100644
View file @
3198867f
export
default
()
=>
({
error
:
null
,
isLoading
:
true
,
// functions
functions
:
[],
hasFunctionData
:
true
,
// function_details
hasPrometheus
:
true
,
hasPrometheusData
:
false
,
graphData
:
{},
});
app/assets/javascripts/serverless/stores/serverless_details_store.js
deleted
100644 → 0
View file @
8e33e7cf
export
default
class
ServerlessDetailsStore
{
constructor
()
{
this
.
state
=
{
functionDetail
:
{},
};
}
updateDetailedFunction
(
func
)
{
this
.
state
.
functionDetail
=
func
;
}
}
app/assets/javascripts/serverless/stores/serverless_store.js
deleted
100644 → 0
View file @
8e33e7cf
export
default
class
ServerlessStore
{
constructor
(
knativeInstalled
=
false
,
clustersPath
,
helpPath
)
{
this
.
state
=
{
functions
:
{},
hasFunctionData
:
true
,
loadingData
:
true
,
installed
:
knativeInstalled
,
clustersPath
,
helpPath
,
};
}
updateFunctionsFromServer
(
upstreamFunctions
=
[])
{
this
.
state
.
functions
=
upstreamFunctions
.
reduce
((
rv
,
func
)
=>
{
const
envs
=
rv
;
envs
[
func
.
environment_scope
]
=
(
rv
[
func
.
environment_scope
]
||
[]).
concat
([
func
]);
return
envs
;
},
{});
}
updateLoadingState
(
loadingData
)
{
this
.
state
.
loadingData
=
loadingData
;
}
toggleNoFunctionData
()
{
this
.
state
.
hasFunctionData
=
false
;
}
}
app/assets/javascripts/serverless/utils.js
0 → 100644
View file @
3198867f
// Validate that the object coming in has valid query details and results
export
const
validateGraphData
=
data
=>
data
.
queries
&&
Array
.
isArray
(
data
.
queries
)
&&
data
.
queries
.
filter
(
query
=>
{
if
(
Array
.
isArray
(
query
.
result
))
{
return
query
.
result
.
filter
(
res
=>
Array
.
isArray
(
res
.
values
)).
length
===
query
.
result
.
length
;
}
return
false
;
}).
length
===
data
.
queries
.
length
;
export
const
translate
=
functions
=>
functions
.
reduce
(
(
acc
,
func
)
=>
Object
.
assign
(
acc
,
{
[
func
.
environment_scope
]:
(
acc
[
func
.
environment_scope
]
||
[]).
concat
([
func
]),
}),
{},
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/controllers/projects/serverless/functions_controller.rb
View file @
3198867f
...
...
@@ -7,19 +7,14 @@ module Projects
before_action
:authorize_read_cluster!
INDEX_PRIMING_INTERVAL
=
15_000
INDEX_POLLING_INTERVAL
=
60_000
def
index
respond_to
do
|
format
|
format
.
json
do
functions
=
finder
.
execute
if
functions
.
any?
Gitlab
::
PollingInterval
.
set_header
(
response
,
interval:
INDEX_POLLING_INTERVAL
)
render
json:
serialize_function
(
functions
)
else
Gitlab
::
PollingInterval
.
set_header
(
response
,
interval:
INDEX_PRIMING_INTERVAL
)
head
:no_content
end
end
...
...
@@ -33,6 +28,8 @@ module Projects
def
show
@service
=
serialize_function
(
finder
.
service
(
params
[
:environment_id
],
params
[
:id
]))
@prometheus
=
finder
.
has_prometheus?
(
params
[
:environment_id
])
return
not_found
if
@service
.
nil?
respond_to
do
|
format
|
...
...
@@ -44,10 +41,24 @@ module Projects
end
end
def
metrics
respond_to
do
|
format
|
format
.
json
do
metrics
=
finder
.
invocation_metrics
(
params
[
:environment_id
],
params
[
:id
])
if
metrics
.
nil?
head
:no_content
else
render
json:
metrics
end
end
end
end
private
def
finder
Projects
::
Serverless
::
FunctionsFinder
.
new
(
project
.
clusters
)
Projects
::
Serverless
::
FunctionsFinder
.
new
(
project
)
end
def
serialize_function
(
function
)
...
...
app/finders/projects/serverless/functions_finder.rb
View file @
3198867f
...
...
@@ -3,8 +3,9 @@
module
Projects
module
Serverless
class
FunctionsFinder
def
initialize
(
clusters
)
@clusters
=
clusters
def
initialize
(
project
)
@clusters
=
project
.
clusters
@project
=
project
end
def
execute
...
...
@@ -19,6 +20,23 @@ module Projects
knative_service
(
environment_scope
,
name
)
&
.
first
end
def
invocation_metrics
(
environment_scope
,
name
)
return
unless
prometheus_adapter
&
.
can_query?
cluster
=
clusters_with_knative_installed
.
preload_knative
.
find
do
|
c
|
environment_scope
==
c
.
environment_scope
end
func
=
::
Serverless
::
Function
.
new
(
@project
,
name
,
cluster
.
platform_kubernetes
&
.
actual_namespace
)
prometheus_adapter
.
query
(
:knative_invocation
,
func
)
end
def
has_prometheus?
(
environment_scope
)
clusters_with_knative_installed
.
preload_knative
.
to_a
.
any?
do
|
cluster
|
environment_scope
==
cluster
.
environment_scope
&&
cluster
.
application_prometheus_available?
end
end
private
def
knative_service
(
environment_scope
,
name
)
...
...
@@ -55,6 +73,12 @@ module Projects
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
end
# rubocop: enable CodeReuse/ServiceClass
end
end
end
app/models/serverless/function.rb
0 → 100644
View file @
3198867f
# frozen_string_literal: true
module
Serverless
class
Function
attr_accessor
:name
,
:namespace
def
initialize
(
project
,
name
,
namespace
)
@project
=
project
@name
=
name
@namespace
=
namespace
end
def
id
@project
.
id
.
to_s
+
"/"
+
@name
+
"/"
+
@namespace
end
def
self
.
find_by_id
(
id
)
array
=
id
.
split
(
"/"
)
project
=
Project
.
find_by_id
(
array
[
0
])
name
=
array
[
1
]
namespace
=
array
[
2
]
self
.
new
(
project
,
name
,
namespace
)
end
end
end
app/serializers/projects/serverless/service_entity.rb
View file @
3198867f
...
...
@@ -32,6 +32,13 @@ module Projects
service
.
dig
(
'podcount'
)
end
expose
:metrics_url
do
|
service
|
project_serverless_metrics_path
(
request
.
project
,
service
.
dig
(
'environment_scope'
),
service
.
dig
(
'metadata'
,
'name'
))
+
".json"
end
expose
:created_at
do
|
service
|
service
.
dig
(
'metadata'
,
'creationTimestamp'
)
end
...
...
app/views/projects/serverless/functions/index.html.haml
View file @
3198867f
...
...
@@ -5,7 +5,10 @@
-
status_path
=
project_serverless_functions_path
(
@project
,
format: :json
)
-
clusters_path
=
project_clusters_path
(
@project
)
.serverless-functions-page.js-serverless-functions-page
{
data:
{
status_path:
status_path
,
installed:
@installed
,
clusters_path:
clusters_path
,
help_path:
help_page_path
(
'user/project/clusters/serverless/index'
)
}
}
.serverless-functions-page.js-serverless-functions-page
{
data:
{
status_path:
status_path
,
installed:
@installed
,
clusters_path:
clusters_path
,
help_path:
help_page_path
(
'user/project/clusters/serverless/index'
)
}
}
%div
{
class:
[
container_class
,
(
'limit-container-width'
unless
fluid_layout
)]
}
.js-serverless-functions-notice
...
...
app/views/projects/serverless/functions/show.html.haml
View file @
3198867f
-
@no_container
=
true
-
@content_class
=
"limit-container-width"
unless
fluid_layout
-
clusters_path
=
project_clusters_path
(
@project
)
-
help_path
=
help_page_path
(
'user/project/clusters/serverless/index'
)
-
add_to_breadcrumbs
(
'Serverless'
,
project_serverless_functions_path
(
@project
))
-
page_title
@service
[
:name
]
.serverless-function-details-page.js-serverless-function-details-page
{
data:
{
service:
@service
.
as_json
}
}
.serverless-function-details-page.js-serverless-function-details-page
{
data:
{
service:
@service
.
as_json
,
prometheus:
@prometheus
,
clusters_path:
clusters_path
,
help_path:
help_path
}
}
%div
{
class:
[
container_class
,
(
'limit-container-width'
unless
fluid_layout
)]
}
.top-area.adjust
.serverless-function-details
#js-serverless-function-details
.js-serverless-function-notice
...
...
changelogs/unreleased/knative-prometheus.yml
0 → 100644
View file @
3198867f
---
title
:
Add Knative metrics to Prometheus
merge_request
:
24663
author
:
Chris Baumbauer <cab@cabnetworks.net>
type
:
added
config/prometheus/common_metrics.yml
View file @
3198867f
...
...
@@ -259,3 +259,13 @@
label
:
Pod average
unit
:
"
cores"
track
:
canary
-
title
:
"
Knative
function
invocations"
y_label
:
"
Invocations"
required_metrics
:
-
istio_revision_request_count
weight
:
1
queries
:
-
id
:
system_metrics_knative_function_invocation_count
query_range
:
'
floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}",
destination_namespace="%{kube_namespace}"}[1m])*30))'
label
:
invocations / minute
unit
:
requests
config/routes/project.rb
View file @
3198867f
...
...
@@ -252,7 +252,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
namespace
:serverless
do
get
'/functions/:environment_id/:id'
,
to:
'functions#show'
scope
:functions
do
get
'/:environment_id/:id'
,
to:
'functions#show'
get
'/:environment_id/:id/metrics'
,
to:
'functions#metrics'
,
as: :metrics
end
resources
:functions
,
only:
[
:index
]
end
...
...
db/migrate/20190326164045_import_common_metrics_knative.rb
0 → 100644
View file @
3198867f
# frozen_string_literal: true
class
ImportCommonMetricsKnative
<
ActiveRecord
::
Migration
[
5.0
]
include
Gitlab
::
Database
::
MigrationHelpers
require
Rails
.
root
.
join
(
'db/importers/common_metrics_importer.rb'
)
DOWNTIME
=
false
def
up
Importers
::
CommonMetricsImporter
.
new
.
execute
end
def
down
# no-op
end
end
db/schema.rb
View file @
3198867f
...
...
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord
::
Schema
.
define
(
version:
2019032
5165127
)
do
ActiveRecord
::
Schema
.
define
(
version:
2019032
6164045
)
do
# These are extensions that must be enabled in order to support this database
enable_extension
"plpgsql"
...
...
doc/user/project/clusters/serverless/img/function-details-loaded.png
0 → 100644
View file @
3198867f
91.3 KB
doc/user/project/clusters/serverless/index.md
View file @
3198867f
...
...
@@ -301,3 +301,23 @@ The second to last line, labeled **Service domain** contains the URL for the dep
browser to see the app live.
![
knative app
](
img/knative-app.png
)
## Function details
Go to the
**Operations > Serverless**
page and click on one of the function
rows to bring up the function details page.
![
function_details
](
img/function-details-loaded.png
)
The pod count will give you the number of pods running the serverless function instances on a given cluster.
### Prometheus support
For the Knative function invocations to appear,
[
Prometheus must be installed
](
../index.md#installing-applications
)
.
Once Prometheus is installed, a message may appear indicating that the metrics data _is
loading or is not available at this time._ It will appear upon the first access of the
page, but should go away after a few seconds. If the message does not disappear, then it
is possible that GitLab is unable to connect to the Prometheus instance running on the
cluster.
lib/gitlab/prometheus/queries/knative_invocation_query.rb
0 → 100644
View file @
3198867f
# frozen_string_literal: true
module
Gitlab
module
Prometheus
module
Queries
class
KnativeInvocationQuery
<
BaseQuery
include
QueryAdditionalMetrics
def
query
(
serverless_function_id
)
PrometheusMetric
.
find_by_identifier
(
:system_metrics_knative_function_invocation_count
)
.
to_query_metric
.
tap
do
|
q
|
q
.
queries
[
0
][
:result
]
=
run_query
(
q
.
queries
[
0
][
:query_range
],
context
(
serverless_function_id
))
end
end
protected
def
context
(
function_id
)
function
=
Serverless
::
Function
.
find_by_id
(
function_id
)
{
function_name:
function
.
name
,
kube_namespace:
function
.
namespace
}
end
def
run_query
(
query
,
context
)
query
%=
context
client_query_range
(
query
,
start:
8
.
hours
.
ago
.
to_f
,
stop:
Time
.
now
.
to_f
)
end
def
self
.
transform_reactive_result
(
result
)
result
[
:metrics
]
=
result
.
delete
:data
result
end
end
end
end
end
locale/gitlab.pot
View file @
3198867f
...
...
@@ -7238,9 +7238,24 @@ msgstr ""
msgid "Serverless"
msgstr ""
msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first."
msgstr ""
msgid "ServerlessDetails|Install Prometheus"
msgstr ""
msgid "ServerlessDetails|Invocation metrics loading or not available at this time."
msgstr ""
msgid "ServerlessDetails|Invocations"
msgstr ""
msgid "ServerlessDetails|Kubernetes Pods"
msgstr ""
msgid "ServerlessDetails|More information"
msgstr ""
msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity."
msgstr ""
...
...
@@ -7256,9 +7271,6 @@ msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
msgstr ""
msgid "Serverless|An error occurred while retrieving serverless components"
msgstr ""
msgid "Serverless|Getting started with serverless"
msgstr ""
...
...
spec/controllers/projects/serverless/functions_controller_spec.rb
View file @
3198867f
...
...
@@ -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
...
...
spec/features/projects/serverless/functions_spec.rb
View file @
3198867f
...
...
@@ -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
spec/finders/projects/serverless/functions_finder_spec.rb
View file @
3198867f
...
...
@@ -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
...
...
spec/javascripts/serverless/components/area_spec.js
0 → 100644
View file @
3198867f
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)
'
);
});
});
});
});
spec/javascripts/serverless/components/environment_row_spec.js
View file @
3198867f
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
();
});
});
});
spec/javascripts/serverless/components/function_details_spec.js
0 → 100644
View file @
3198867f
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
);
});
});
});
spec/javascripts/serverless/components/function_row_spec.js
View file @
3198867f
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
();
});
...
...
spec/javascripts/serverless/components/functions_spec.js
View file @
3198867f
import
Vue
from
'
vue
'
;
import
Vue
x
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
);
});
});
});
spec/javascripts/serverless/components/missing_prometheus_spec.js
0 → 100644
View file @
3198867f
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.
'
,
);
});
});
spec/javascripts/serverless/components/pod_box_spec.js
0 → 100644
View file @
3198867f
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
();
});
});
spec/javascripts/serverless/components/url_spec.js
View file @
3198867f
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
,
{
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
);
...
...
spec/javascripts/serverless/mock_data.js
View file @
3198867f
...
...
@@ -77,3 +77,60 @@ export const mockMultilineServerlessFunction = {
description
:
'
testfunc1
\n
A 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
,
},
],
},
],
},
],
};
spec/javascripts/serverless/store/actions_spec.js
0 → 100644
View file @
3198867f
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
();
},
);
});
});
});
spec/javascripts/serverless/store/getters_spec.js
0 → 100644
View file @
3198867f
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
);
});
});
});
spec/javascripts/serverless/store/mutations_spec.js
0 → 100644
View file @
3198867f
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
);
});
});
});
spec/javascripts/serverless/stores/serverless_store_spec.js
deleted
100644 → 0
View file @
8e33e7cf
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
'
);
});
});
});
spec/javascripts/serverless/utils.js
0 → 100644
View file @
3198867f
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
()
=>
{};
spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
0 → 100644
View file @
3198867f
# 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
spec/models/serverless/function_spec.rb
0 → 100644
View file @
3198867f
# 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
spec/support/helpers/prometheus_helpers.rb
View file @
3198867f
...
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment