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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
b77fe7db
Commit
b77fe7db
authored
Apr 06, 2019
by
Chris Baumbauer
Committed by
Mike Greiling
Apr 06, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add Knative metrics to Prometheus
parent
8e33e7cf
Changes
52
Hide 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 @
b77fe7db
<
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 @
b77fe7db
<
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 @
b77fe7db
<
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 @
b77fe7db
<
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,17 +25,23 @@ export default {
type
:
String
,
required
:
true
,
},
loadingData
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
hasFunctionData
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
statusPath
:
{
type
:
String
,
required
:
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 @
b77fe7db
<
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 @
b77fe7db
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 @
b77fe7db
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
,
};
},
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
,
},
});
},
});
}
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
();
}
const
el
=
document
.
querySelector
(
'
#js-serverless-functions
'
);
this
.
functions
=
new
Vue
({
el
,
store
:
createStore
(),
render
(
createElement
)
{
return
createElement
(
Functions
,
{
props
:
{
installed
:
installed
!==
undefined
,
clustersPath
,
helpPath
,
statusPath
,
},
});
},
});
}
}
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
// 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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
# 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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
-
@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
.serverless-function-details
#js-serverless-function-details
.js-serverless-function-notice
.flash-container
...
...
changelogs/unreleased/knative-prometheus.yml
0 → 100644
View file @
b77fe7db
---
title
:
Add Knative metrics to Prometheus
merge_request
:
24663
author
:
Chris Baumbauer <cab@cabnetworks.net>
type
:
added
config/prometheus/common_metrics.yml
View file @
b77fe7db
...
...
@@ -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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
# 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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
91.3 KB
doc/user/project/clusters/serverless/index.md
View file @
b77fe7db
...
...
@@ -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 @
b77fe7db
# 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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
...
...
@@ -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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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
);
...
...
spec/javascripts/serverless/mock_data.js
View file @
b77fe7db
...
...
@@ -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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
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 @
b77fe7db
# 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 @
b77fe7db
# 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 @
b77fe7db
...
...
@@ -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