Commit 1b748440 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'pawel/connect_to_prometheus_through_proxy-30480' into 'master'

Deploy prometheus through kubernetes and autoconnect to cluster

Closes #30480 and #28916

See merge request gitlab-org/gitlab-ce!16182
parents bb6895ec 0e90284c
...@@ -32,6 +32,7 @@ export default class Clusters { ...@@ -32,6 +32,7 @@ export default class Clusters {
installIngressPath, installIngressPath,
installRunnerPath, installRunnerPath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath,
clusterStatus, clusterStatus,
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
...@@ -40,6 +41,7 @@ export default class Clusters { ...@@ -40,6 +41,7 @@ export default class Clusters {
this.store = new ClustersStore(); this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath); this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus); this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason); this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({ this.service = new ClustersService({
...@@ -95,6 +97,7 @@ export default class Clusters { ...@@ -95,6 +97,7 @@ export default class Clusters {
applications: this.state.applications, applications: this.state.applications,
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath, ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
}, },
}); });
}, },
......
...@@ -32,6 +32,10 @@ ...@@ -32,6 +32,10 @@
type: String, type: String,
required: false, required: false,
}, },
manageLink: {
type: String,
required: false,
},
description: { description: {
type: String, type: String,
required: true, required: true,
...@@ -89,6 +93,12 @@ ...@@ -89,6 +93,12 @@
return label; return label;
}, },
showManageButton() {
return this.manageLink && this.status === APPLICATION_INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() { hasError() {
return this.status === APPLICATION_ERROR || return this.status === APPLICATION_ERROR ||
this.requestStatus === REQUEST_FAILURE; this.requestStatus === REQUEST_FAILURE;
...@@ -141,9 +151,21 @@ ...@@ -141,9 +151,21 @@
<div v-html="description"></div> <div v-html="description"></div>
</div> </div>
<div <div
class="table-section table-button-footer section-15 section-align-top" class="table-section table-button-footer section-align-top"
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
role="gridcell" role="gridcell"
> >
<div
v-if="showManageButton"
class="btn-group table-action-buttons"
>
<a
class="btn"
:href="manageLink"
>
{{ manageButtonLabel }}
</a>
</div>
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
class="js-cluster-application-install-button" class="js-cluster-application-install-button"
......
...@@ -23,13 +23,19 @@ ...@@ -23,13 +23,19 @@
required: false, required: false,
default: '', default: '',
}, },
managePrometheusPath: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
generalApplicationDescription() { generalApplicationDescription() {
return sprintf( return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster. _.escape(s__(
Read more about %{helpLink}`)), `ClusterIntegration|Install applications on your Kubernetes cluster.
{ Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}"> helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))} ${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`, </a>`,
...@@ -96,11 +102,12 @@ ...@@ -96,11 +102,12 @@
}, },
prometheusDescription() { prometheusDescription() {
return sprintf( return sprintf(
_.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system _.escape(s__(
with %{gitlabIntegrationLink} to monitor deployed applications.`)), `ClusterIntegration|Prometheus is an open-source monitoring system
{ with %{gitlabIntegrationLink} to monitor deployed applications.`,
)), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer"> target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
}, },
false, false,
...@@ -149,6 +156,7 @@ target="_blank" rel="noopener noreferrer"> ...@@ -149,6 +156,7 @@ target="_blank" rel="noopener noreferrer">
id="prometheus" id="prometheus"
:title="applications.prometheus.title" :title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
:description="prometheusDescription" :description="prometheusDescription"
:status="applications.prometheus.status" :status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
......
...@@ -45,6 +45,10 @@ export default class ClusterStore { ...@@ -45,6 +45,10 @@ export default class ClusterStore {
this.state.ingressHelpPath = ingressHelpPath; this.state.ingressHelpPath = ingressHelpPath;
} }
setManagePrometheusPath(managePrometheusPath) {
this.state.managePrometheusPath = managePrometheusPath;
}
updateStatus(status) { updateStatus(status) {
this.state.status = status; this.state.status = status;
} }
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath, documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath, settingsPath: metricsData.settingsPath,
clustersPath: metricsData.clustersPath,
tagsPath: metricsData.tagsPath, tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath, projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics, metricsEndpoint: metricsData.additionalMetrics,
...@@ -132,6 +133,7 @@ ...@@ -132,6 +133,7 @@
:selected-state="state" :selected-state="state"
:documentation-path="documentationPath" :documentation-path="documentationPath"
:settings-path="settingsPath" :settings-path="settingsPath"
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
......
...@@ -10,6 +10,11 @@ ...@@ -10,6 +10,11 @@
required: false, required: false,
default: '', default: '',
}, },
clustersPath: {
type: String,
required: false,
default: '',
},
selectedState: { selectedState: {
type: String, type: String,
required: true, required: true,
...@@ -35,7 +40,10 @@ ...@@ -35,7 +40,10 @@
title: 'Get started with performance monitoring', title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`, of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Configure Prometheus', buttonText: 'Install Prometheus on clusters',
buttonPath: this.clustersPath,
secondaryButtonText: 'Configure existing Prometheus',
secondaryButtonPath: this.settingsPath,
}, },
loading: { loading: {
svgUrl: this.emptyLoadingSvgPath, svgUrl: this.emptyLoadingSvgPath,
...@@ -43,6 +51,7 @@ ...@@ -43,6 +51,7 @@
description: `Creating graphs uses the data from the Prometheus server. description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`, If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation', buttonText: 'View documentation',
buttonPath: this.documentationPath,
}, },
noData: { noData: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
...@@ -50,12 +59,14 @@ ...@@ -50,12 +59,14 @@
description: `You are connected to the Prometheus server, but there is currently description: `You are connected to the Prometheus server, but there is currently
no data to display.`, no data to display.`,
buttonText: 'Configure Prometheus', buttonText: 'Configure Prometheus',
buttonPath: this.settingsPath,
}, },
unableToConnect: { unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server', title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ', description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation', buttonText: 'View documentation',
buttonPath: this.documentationPath,
}, },
}, },
}; };
...@@ -65,13 +76,6 @@ ...@@ -65,13 +76,6 @@
return this.states[this.selectedState]; return this.states[this.selectedState];
}, },
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() { showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true; if (this.selectedState === 'unableToConnect') return true;
return false; return false;
...@@ -99,11 +103,21 @@ ...@@ -99,11 +103,21 @@
</p> </p>
<div class="state-button"> <div class="state-button">
<a <a
v-if="currentState.buttonPath"
class="btn btn-success" class="btn btn-success"
:href="buttonPath" :href="currentState.buttonPath"
> >
{{ currentState.buttonText }} {{ currentState.buttonText }}
</a> </a>
</div> </div>
<div class="state-button">
<a
v-if="currentState.secondaryButtonPath"
class="btn"
:href="currentState.secondaryButtonPath"
>
{{ currentState.secondaryButtonText }}
</a>
</div>
</div> </div>
</template> </template>
...@@ -205,7 +205,7 @@ ...@@ -205,7 +205,7 @@
} }
.prometheus-state { .prometheus-state {
max-width: 430px; max-width: 460px;
margin: 10px auto; margin: 10px auto;
text-align: center; text-align: center;
...@@ -213,6 +213,10 @@ ...@@ -213,6 +213,10 @@
max-width: 80vw; max-width: 80vw;
margin: 0 auto; margin: 0 auto;
} }
.state-button {
padding: $gl-padding / 2;
}
} }
.environments-actions { .environments-actions {
......
...@@ -135,6 +135,17 @@ ...@@ -135,6 +135,17 @@
padding-top: 0; padding-top: 0;
} }
.integration-settings-form {
.well {
padding: $gl-padding / 2;
box-shadow: none;
}
.svg-container {
max-width: 150px;
}
}
.token-token-container { .token-token-container {
#impersonation-token-token { #impersonation-token-token {
width: 80%; width: 80%;
......
...@@ -32,6 +32,7 @@ module ServiceParams ...@@ -32,6 +32,7 @@ module ServiceParams
:issues_events, :issues_events,
:issues_url, :issues_url,
:jira_issue_transition_id, :jira_issue_transition_id,
:manual_configuration,
:merge_requests_events, :merge_requests_events,
:mock_service_url, :mock_service_url,
:namespace, :namespace,
......
...@@ -10,10 +10,26 @@ module Clusters ...@@ -10,10 +10,26 @@ module Clusters
default_value_for :version, VERSION default_value_for :version, VERSION
state_machine :status do
after_transition any => [:installed] do |application|
application.cluster.projects.each do |project|
project.find_or_initialize_service('prometheus').update(active: true)
end
end
end
def chart def chart
'stable/prometheus' 'stable/prometheus'
end end
def service_name
'prometheus-prometheus-server'
end
def service_port
80
end
def chart_values_file def chart_values_file
"#{Rails.root}/vendor/#{name}/values.yaml" "#{Rails.root}/vendor/#{name}/values.yaml"
end end
...@@ -21,6 +37,22 @@ module Clusters ...@@ -21,6 +37,22 @@ module Clusters
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end end
def proxy_client
return unless kube_client
proxy_url = kube_client.proxy_url('service', service_name, service_port, Gitlab::Kubernetes::Helm::NAMESPACE)
# ensures headers containing auth data are appended to original k8s client options
options = kube_client.rest_client.options.merge(headers: kube_client.headers)
RestClient::Resource.new(proxy_url, options)
end
private
def kube_client
cluster&.kubeclient
end
end end
end end
end end
...@@ -49,6 +49,9 @@ module Clusters ...@@ -49,6 +49,9 @@ module Clusters
scope :enabled, -> { where(enabled: true) } scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) } scope :disabled, -> { where(enabled: false) }
scope :for_environment, -> (env) { where(environment_scope: ['*', '', env.slug]) }
scope :for_all_environments, -> { where(environment_scope: ['*', '']) }
def status_name def status_name
if provider if provider
provider.status_name provider.status_name
......
...@@ -7,11 +7,14 @@ class PrometheusService < MonitoringService ...@@ -7,11 +7,14 @@ class PrometheusService < MonitoringService
# Access to prometheus is directly through the API # Access to prometheus is directly through the API
prop_accessor :api_url prop_accessor :api_url
boolean_accessor :manual_configuration
with_options presence: true, if: :activated? do with_options presence: true, if: :manual_configuration? do
validates :api_url, url: true validates :api_url, url: true
end end
before_save :synchronize_service_state!
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
def initialize_properties def initialize_properties
...@@ -20,12 +23,20 @@ class PrometheusService < MonitoringService ...@@ -20,12 +23,20 @@ class PrometheusService < MonitoringService
end end
end end
def show_active_box?
false
end
def editable?
manual_configuration? || !prometheus_installed?
end
def title def title
'Prometheus' 'Prometheus'
end end
def description def description
s_('PrometheusService|Prometheus monitoring') s_('PrometheusService|Time-series monitoring service')
end end
def self.to_param def self.to_param
...@@ -33,7 +44,15 @@ class PrometheusService < MonitoringService ...@@ -33,7 +44,15 @@ class PrometheusService < MonitoringService
end end
def fields def fields
return [] unless editable?
[ [
{
type: 'checkbox',
name: 'manual_configuration',
title: s_('PrometheusService|Active'),
required: true
},
{ {
type: 'text', type: 'text',
name: 'api_url', name: 'api_url',
...@@ -59,7 +78,7 @@ class PrometheusService < MonitoringService ...@@ -59,7 +78,7 @@ class PrometheusService < MonitoringService
end end
def deployment_metrics(deployment) def deployment_metrics(deployment)
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics)) metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.environment.id, deployment.id, &method(:rename_data_to_metrics))
metrics&.merge(deployment_time: deployment.created_at.to_i) || {} metrics&.merge(deployment_time: deployment.created_at.to_i) || {}
end end
...@@ -68,7 +87,7 @@ class PrometheusService < MonitoringService ...@@ -68,7 +87,7 @@ class PrometheusService < MonitoringService
end end
def additional_deployment_metrics(deployment) def additional_deployment_metrics(deployment)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.id, &:itself) with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.environment.id, deployment.id, &:itself)
end end
def matched_metrics def matched_metrics
...@@ -79,6 +98,9 @@ class PrometheusService < MonitoringService ...@@ -79,6 +98,9 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(query_class_name, *args) def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete? return unless active? && project && !project.pending_delete?
environment_id = args.first
client = client(environment_id)
data = Kernel.const_get(query_class_name).new(client).query(*args) data = Kernel.const_get(query_class_name).new(client).query(*args)
{ {
success: true, success: true,
...@@ -89,14 +111,55 @@ class PrometheusService < MonitoringService ...@@ -89,14 +111,55 @@ class PrometheusService < MonitoringService
{ success: false, result: err.message } { success: false, result: err.message }
end end
def client def client(environment_id = nil)
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url) if manual_configuration?
Gitlab::PrometheusClient.new(RestClient::Resource.new(api_url))
else
cluster = cluster_with_prometheus(environment_id)
raise Gitlab::PrometheusError, "couldn't find cluster with Prometheus installed" unless cluster
rest_client = client_from_cluster(cluster)
raise Gitlab::PrometheusError, "couldn't create proxy Prometheus client" unless rest_client
Gitlab::PrometheusClient.new(rest_client)
end
end
def prometheus_installed?
return false if template?
return false unless project
project.clusters.enabled.any? { |cluster| cluster.application_prometheus&.installed? }
end end
private private
def cluster_with_prometheus(environment_id = nil)
clusters = if environment_id
::Environment.find_by(id: environment_id).try do |env|
# sort results by descending order based on environment_scope being longer
# thus more closely matching environment slug
project.clusters.enabled.for_environment(env).sort_by { |c| c.environment_scope&.length }.reverse!
end
else
project.clusters.enabled.for_all_environments
end
clusters&.detect { |cluster| cluster.application_prometheus&.installed? }
end
def client_from_cluster(cluster)
cluster.application_prometheus.proxy_client
end
def rename_data_to_metrics(metrics) def rename_data_to_metrics(metrics)
metrics[:metrics] = metrics.delete :data metrics[:metrics] = metrics.delete :data
metrics metrics
end end
def synchronize_service_state!
self.active = prometheus_installed? || manual_configuration?
true
end
end end
...@@ -14,7 +14,8 @@ ...@@ -14,7 +14,8 @@
cluster_status: @cluster.status_name, cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address') } } ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice .js-cluster-application-notice
.flash-container .flash-container
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
= link_to @environment.name, environment_path(@environment) = link_to @environment.name, environment_path(@environment)
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
%p= @service.description %p= @service.description
.col-lg-9 .col-lg-9
= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, subject: @service = render 'shared/service_settings', form: form, subject: @service
- if @service.editable? - if @service.editable?
.footer-block.row-content-block .footer-block.row-content-block
......
%h4
= s_('PrometheusService|Auto configuration')
- if @service.manual_configuration?
.well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
.row
- if @service.prometheus_installed?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
.col-sm-10
%p.text-success.prepend-top-default
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
.col-sm-10
%p.prepend-top-default
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success'
%hr
%h4.append-bottom-default
= s_('PrometheusService|Manual configuration')
- unless @service.editable?
.well
= s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters')
---
title: Implement multi server support and use kube proxy to connect to Prometheus
servers inside K8S cluster
merge_request: 16182
author:
type: added
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
class AdditionalMetricsDeploymentQuery < BaseQuery class AdditionalMetricsDeploymentQuery < BaseQuery
include QueryAdditionalMetrics include QueryAdditionalMetrics
def query(deployment_id) def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment| Deployment.find_by(id: deployment_id).try do |deployment|
query_metrics( query_metrics(
common_query_context( common_query_context(
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module Prometheus module Prometheus
module Queries module Queries
class DeploymentQuery < BaseQuery class DeploymentQuery < BaseQuery
def query(deployment_id) def query(environment_id, deployment_id)
Deployment.find_by(id: deployment_id).try do |deployment| Deployment.find_by(id: deployment_id).try do |deployment|
environment_slug = deployment.environment.slug environment_slug = deployment.environment.slug
......
...@@ -3,10 +3,10 @@ module Gitlab ...@@ -3,10 +3,10 @@ module Gitlab
# Helper methods to interact with Prometheus network services & resources # Helper methods to interact with Prometheus network services & resources
class PrometheusClient class PrometheusClient
attr_reader :api_url attr_reader :rest_client, :headers
def initialize(api_url:) def initialize(rest_client)
@api_url = api_url @rest_client = rest_client
end end
def ping def ping
...@@ -40,37 +40,40 @@ module Gitlab ...@@ -40,37 +40,40 @@ module Gitlab
private private
def json_api_get(type, args = {}) def json_api_get(type, args = {})
get(join_api_url(type, args)) path = ['api', 'v1', type].join('/')
get(path, args)
rescue JSON::ParserError
raise PrometheusError, 'Parsing response failed'
rescue Errno::ECONNREFUSED rescue Errno::ECONNREFUSED
raise PrometheusError, 'Connection refused' raise PrometheusError, 'Connection refused'
end end
def join_api_url(type, args = {}) def get(path, args)
url = URI.parse(api_url) response = rest_client[path].get(params: args)
rescue URI::Error handle_response(response)
raise PrometheusError, "Invalid API URL: #{api_url}"
else
url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
url.query = args.to_query
url.to_s
end
def get(url)
handle_response(HTTParty.get(url))
rescue SocketError rescue SocketError
raise PrometheusError, "Can't connect to #{url}" raise PrometheusError, "Can't connect to #{rest_client.url}"
rescue OpenSSL::SSL::SSLError rescue OpenSSL::SSL::SSLError
raise PrometheusError, "#{url} contains invalid SSL data" raise PrometheusError, "#{rest_client.url} contains invalid SSL data"
rescue HTTParty::Error rescue RestClient::ExceptionWithResponse => ex
handle_exception_response(ex.response)
rescue RestClient::Exception
raise PrometheusError, "Network connection error" raise PrometheusError, "Network connection error"
end end
def handle_response(response) def handle_response(response)
if response.code == 200 && response['status'] == 'success' json_data = JSON.parse(response.body)
response['data'] || {} if response.code == 200 && json_data['status'] == 'success'
elsif response.code == 400 json_data['data'] || {}
raise PrometheusError, response['error'] || 'Bad data received' else
raise PrometheusError, "#{response.code} - #{response.body}"
end
end
def handle_exception_response(response)
if response.code == 400
json_data = JSON.parse(response.body)
raise PrometheusError, json_data['error'] || 'Bad data received'
else else
raise PrometheusError, "#{response.code} - #{response.body}" raise PrometheusError, "#{response.code} - #{response.body}"
end end
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2141,7 +2141,7 @@ msgstr "" ...@@ -2141,7 +2141,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "Nessuna metrica è stata monitorata. Per iniziare a monitorare, rilascia ...@@ -2018,7 +2018,7 @@ msgstr "Nessuna metrica è stata monitorata. Per iniziare a monitorare, rilascia
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "" ...@@ -2018,7 +2018,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2032,7 +2032,7 @@ msgstr "" ...@@ -2032,7 +2032,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2018,7 +2018,7 @@ msgstr "Nenhuma métrica está sendo monitorada. Para inicar o monitoramento, fa ...@@ -2018,7 +2018,7 @@ msgstr "Nenhuma métrica está sendo monitorada. Para inicar o monitoramento, fa
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "URL da API base do Prometheus. como http://prometheus.example.com/" msgstr "URL da API base do Prometheus. como http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "Monitoramento com Prometheus" msgstr "Monitoramento com Prometheus"
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2032,7 +2032,7 @@ msgstr "" ...@@ -2032,7 +2032,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2032,7 +2032,7 @@ msgstr "Жодні метрики не відслідковуються. Для ...@@ -2032,7 +2032,7 @@ msgstr "Жодні метрики не відслідковуються. Для
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Базова адреса Prometheus API, наприклад http://prometheus.example.com/" msgstr "Базова адреса Prometheus API, наприклад http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "Моніторинг Prometheus" msgstr "Моніторинг Prometheus"
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "没有监测指标。要开始监测,请部署到环境中。" ...@@ -2004,7 +2004,7 @@ msgstr "没有监测指标。要开始监测,请部署到环境中。"
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "Prometheus API 地址,例如 http://prometheus.example.com/" msgstr "Prometheus API 地址,例如 http://prometheus.example.com/"
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "Prometheus 监测" msgstr "Prometheus 监测"
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -2004,7 +2004,7 @@ msgstr "" ...@@ -2004,7 +2004,7 @@ msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/" msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr "" msgstr ""
msgid "PrometheusService|Prometheus monitoring" msgid "PrometheusService|Time-series monitoring service"
msgstr "" msgstr ""
msgid "PrometheusService|View environments" msgid "PrometheusService|View environments"
......
...@@ -249,7 +249,8 @@ FactoryBot.define do ...@@ -249,7 +249,8 @@ FactoryBot.define do
project.create_prometheus_service( project.create_prometheus_service(
active: true, active: true,
properties: { properties: {
api_url: 'https://prometheus.example.com' api_url: 'https://prometheus.example.com/',
manual_configuration: true
} }
) )
end end
......
...@@ -30,7 +30,8 @@ FactoryBot.define do ...@@ -30,7 +30,8 @@ FactoryBot.define do
project project
active true active true
properties({ properties({
api_url: 'https://prometheus.example.com/' api_url: 'https://prometheus.example.com/',
manual_configuration: true
}) })
end end
......
...@@ -29,34 +29,6 @@ describe('EmptyState', () => { ...@@ -29,34 +29,6 @@ describe('EmptyState', () => {
expect(component.currentState).toBe(component.states.gettingStarted); expect(component.currentState).toBe(component.states.gettingStarted);
}); });
it('buttonPath returns settings path for the state "gettingStarted"', () => {
const component = createComponent({
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo',
emptyUnableToConnectSvgPath: 'foo',
});
expect(component.buttonPath).toEqual(statePaths.settingsPath);
expect(component.buttonPath).not.toEqual(statePaths.documentationPath);
});
it('buttonPath returns documentation path for any of the other states', () => {
const component = createComponent({
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo',
emptyUnableToConnectSvgPath: 'foo',
});
expect(component.buttonPath).toEqual(statePaths.documentationPath);
expect(component.buttonPath).not.toEqual(statePaths.settingsPath);
});
it('showButtonDescription returns a description with a link for the unableToConnect state', () => { it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
const component = createComponent({ const component = createComponent({
selectedState: 'unableToConnect', selectedState: 'unableToConnect',
...@@ -88,6 +60,7 @@ describe('EmptyState', () => { ...@@ -88,6 +60,7 @@ describe('EmptyState', () => {
const component = createComponent({ const component = createComponent({
selectedState: 'gettingStarted', selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath, settingsPath: statePaths.settingsPath,
clustersPath: statePaths.clustersPath,
documentationPath: statePaths.documentationPath, documentationPath: statePaths.documentationPath,
emptyGettingStartedSvgPath: 'foo', emptyGettingStartedSvgPath: 'foo',
emptyLoadingSvgPath: 'foo', emptyLoadingSvgPath: 'foo',
......
...@@ -2471,6 +2471,7 @@ export const deploymentData = [ ...@@ -2471,6 +2471,7 @@ export const deploymentData = [
export const statePaths = { export const statePaths = {
settingsPath: '/root/hello-prometheus/services/prometheus/edit', settingsPath: '/root/hello-prometheus/services/prometheus/edit',
clustersPath: '/root/hello-prometheus/clusters',
documentationPath: '/help/administration/monitoring/prometheus/index.md', documentationPath: '/help/administration/monitoring/prometheus/index.md',
}; };
......
...@@ -7,7 +7,7 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do ...@@ -7,7 +7,7 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do
include_examples 'additional metrics query' do include_examples 'additional metrics query' do
let(:deployment) { create(:deployment, environment: environment) } let(:deployment) { create(:deployment, environment: environment) }
let(:query_params) { [deployment.id] } let(:query_params) { [environment.id, deployment.id] }
it 'queries using specific time' do it 'queries using specific time' do
expect(client).to receive(:query_range).with(anything, expect(client).to receive(:query_range).with(anything,
......
...@@ -31,7 +31,7 @@ describe Gitlab::Prometheus::Queries::DeploymentQuery do ...@@ -31,7 +31,7 @@ describe Gitlab::Prometheus::Queries::DeploymentQuery do
expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100',
time: stop_time) time: stop_time)
expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, expect(subject.query(environment.id, deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil,
cpu_values: nil, cpu_before: nil, cpu_after: nil) cpu_values: nil, cpu_before: nil, cpu_after: nil)
end end
end end
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::PrometheusClient do describe Gitlab::PrometheusClient do
include PrometheusHelpers include PrometheusHelpers
subject { described_class.new(api_url: 'https://prometheus.example.com') } subject { described_class.new(RestClient::Resource.new('https://prometheus.example.com')) }
describe '#ping' do describe '#ping' do
it 'issues a "query" request to the API endpoint' do it 'issues a "query" request to the API endpoint' do
...@@ -47,16 +47,28 @@ describe Gitlab::PrometheusClient do ...@@ -47,16 +47,28 @@ describe Gitlab::PrometheusClient do
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
end end
context 'when request returns non json data' do
it 'raises a Gitlab::PrometheusError error' do
req_stub = stub_prometheus_request(query_url, status: 200, body: 'not json')
expect { execute_query }
.to raise_error(Gitlab::PrometheusError, 'Parsing response failed')
expect(req_stub).to have_been_requested
end
end
end end
describe 'failure to reach a provided prometheus url' do describe 'failure to reach a provided prometheus url' do
let(:prometheus_url) {"https://prometheus.invalid.example.com"} let(:prometheus_url) {"https://prometheus.invalid.example.com"}
subject { described_class.new(RestClient::Resource.new(prometheus_url)) }
context 'exceptions are raised' do context 'exceptions are raised' do
it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
expect { subject.send(:get, prometheus_url) } expect { subject.send(:get, '/', {}) }
.to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}") .to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
...@@ -64,15 +76,15 @@ describe Gitlab::PrometheusClient do ...@@ -64,15 +76,15 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
expect { subject.send(:get, prometheus_url) } expect { subject.send(:get, '/', {}) }
.to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data") .to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
it 'raises a Gitlab::PrometheusError error when a HTTParty::Error is rescued' do it 'raises a Gitlab::PrometheusError error when a RestClient::Exception is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, HTTParty::Error) req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception)
expect { subject.send(:get, prometheus_url) } expect { subject.send(:get, '/', {}) }
.to raise_error(Gitlab::PrometheusError, "Network connection error") .to raise_error(Gitlab::PrometheusError, "Network connection error")
expect(req_stub).to have_been_requested expect(req_stub).to have_been_requested
end end
......
...@@ -6,6 +6,24 @@ describe Clusters::Applications::Prometheus do ...@@ -6,6 +6,24 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application specs', described_class include_examples 'cluster application specs', described_class
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, projects: [project]) }
let(:prometheus_service) { double('prometheus_service') }
subject { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
before do
allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service
end
it 'ensures Prometheus service is activated' do
expect(prometheus_service).to receive(:update).with(active: true)
subject.make_installed
end
end
describe "#chart_values_file" do describe "#chart_values_file" do
subject { create(:clusters_applications_prometheus).chart_values_file } subject { create(:clusters_applications_prometheus).chart_values_file }
...@@ -13,4 +31,58 @@ describe Clusters::Applications::Prometheus do ...@@ -13,4 +31,58 @@ describe Clusters::Applications::Prometheus do
expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml") expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
end end
end end
describe '#proxy_client' do
context 'cluster is nil' do
it 'returns nil' do
expect(subject.cluster).to be_nil
expect(subject.proxy_client).to be_nil
end
end
context "cluster doesn't have kubeclient" do
let(:cluster) { create(:cluster) }
subject { create(:clusters_applications_prometheus, cluster: cluster) }
it 'returns nil' do
expect(subject.proxy_client).to be_nil
end
end
context 'cluster has kubeclient' do
let(:kubernetes_url) { 'http://example.com' }
let(:k8s_discover_response) do
{
resources: [
{
name: 'service',
kind: 'Service'
}
]
}
end
let(:kube_client) { Kubeclient::Client.new(kubernetes_url) }
let(:cluster) { create(:cluster) }
subject { create(:clusters_applications_prometheus, cluster: cluster) }
before do
allow(kube_client.rest_client).to receive(:get).and_return(k8s_discover_response.to_json)
allow(subject.cluster).to receive(:kubeclient).and_return(kube_client)
end
it 'creates proxy prometheus rest client' do
expect(subject.proxy_client).to be_instance_of(RestClient::Resource)
end
it 'creates proper url' do
expect(subject.proxy_client.url).to eq('http://example.com/api/v1/proxy/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80')
end
it 'copies options and headers from kube client to proxy client' do
expect(subject.proxy_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers))
end
end
end
end end
...@@ -13,6 +13,12 @@ module ReactiveCachingHelpers ...@@ -13,6 +13,12 @@ module ReactiveCachingHelpers
write_reactive_cache(subject, data, *qualifiers) if data write_reactive_cache(subject, data, *qualifiers) if data
end end
def synchronous_reactive_cache(subject)
allow(service).to receive(:with_reactive_cache) do |*args, &block|
block.call(service.calculate_reactive_cache(*args))
end
end
def read_reactive_cache(subject, *qualifiers) def read_reactive_cache(subject, *qualifiers)
Rails.cache.read(reactive_cache_key(subject, *qualifiers)) Rails.cache.read(reactive_cache_key(subject, *qualifiers))
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment