Commit 0d454802 authored by Mayra Cabrera's avatar Mayra Cabrera Committed by Kamil Trzciński

Extend Cluster Applications to allow installation of Prometheus

parent 79cbfedf
......@@ -30,6 +30,7 @@ export default class Clusters {
installHelmPath,
installIngressPath,
installRunnerPath,
installPrometheusPath,
clusterStatus,
clusterStatusReason,
helpPath,
......@@ -44,6 +45,7 @@ export default class Clusters {
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
});
this.toggle = this.toggle.bind(this);
......
......@@ -67,6 +67,16 @@ export default {
and send the results back to GitLab.`,
));
},
prometheusDescription() {
return sprintf(
_.escape(s__('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", target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
</a>`,
},
false,
);
},
},
};
</script>
......@@ -105,6 +115,16 @@ export default {
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
/>
<application-row
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
/>
<!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
<!-- Add GitLab Runner row, all other plumbing is complete -->
......
......@@ -7,6 +7,7 @@ export default class ClusterService {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
};
}
......
......@@ -28,6 +28,13 @@ export default class ClusterStore {
requestStatus: null,
requestReason: null,
},
prometheus: {
title: s__('ClusterIntegration|Prometheus'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
},
},
};
}
......
......@@ -6,7 +6,7 @@
.cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block
min-height: 302px;
min-height: 400px;
}
.clusters-dropdown-menu {
......
......@@ -3,32 +3,19 @@ module Clusters
class Helm < ActiveRecord::Base
self.table_name = 'clusters_applications_helm'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
validates :cluster, presence: true
after_initialize :set_initial_status
def self.application_name
self.to_s.demodulize.underscore
end
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.platform_kubernetes_active?
end
def name
self.class.application_name
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, true)
Gitlab::Kubernetes::Helm::InstallCommand.new(name, install_helm: true)
end
end
end
......
......@@ -3,41 +3,22 @@ module Clusters
class Ingress < ActiveRecord::Base
self.table_name = 'clusters_applications_ingress'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
validates :cluster, presence: true
default_value_for :ingress_type, :nginx
default_value_for :version, :nginx
after_initialize :set_initial_status
enum ingress_type: {
nginx: 1
}
def self.application_name
self.to_s.demodulize.underscore
end
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.application_helm_installed?
end
def name
self.class.application_name
end
def chart
'stable/nginx-ingress'
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, false, chart)
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart)
end
end
end
......
module Clusters
module Applications
class Prometheus < ActiveRecord::Base
VERSION = "2.0.0".freeze
self.table_name = 'clusters_applications_prometheus'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
default_value_for :version, VERSION
def chart
'stable/prometheus'
end
def chart_values_file
"#{Rails.root}/vendor/#{name}/values.yaml"
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end
end
end
end
......@@ -6,7 +6,8 @@ module Clusters
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress
Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus
}.freeze
belongs_to :user
......@@ -21,6 +22,7 @@ module Clusters
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
......@@ -62,7 +64,8 @@ module Clusters
def applications
[
application_helm || build_application_helm,
application_ingress || build_application_ingress
application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus
]
end
......
module Clusters
module Concerns
module ApplicationCore
extend ActiveSupport::Concern
included do
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
validates :cluster, presence: true
after_initialize :set_initial_status
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.application_helm_installed?
end
def self.application_name
self.to_s.demodulize.underscore
end
def name
self.class.application_name
end
end
end
end
end
......@@ -18,7 +18,7 @@ module Clusters
end
def helm_api
@helm_api ||= Gitlab::Kubernetes::Helm.new(kubeclient)
@helm_api ||= Gitlab::Kubernetes::Helm::Api.new(kubeclient)
end
def install_command
......
......@@ -9,6 +9,7 @@
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
......
---
title: Add Prometheus to available Cluster applications
merge_request: 15895
author:
type: added
class CreateClustersApplicationsPrometheus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :clusters_applications_prometheus do |t|
t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
t.integer :status, null: false
t.string :version, null: false
t.text :status_reason
t.timestamps_with_timezone null: false
end
end
end
......@@ -568,6 +568,15 @@ ActiveRecord::Schema.define(version: 20171220191323) do
t.text "status_reason"
end
create_table "clusters_applications_prometheus", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
t.string "version", null: false
t.text "status_reason"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
end
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
......
module Gitlab
module Kubernetes
class Helm
module Helm
HELM_VERSION = '2.7.0'.freeze
NAMESPACE = 'gitlab-managed-apps'.freeze
INSTALL_DEPS = <<-EOS.freeze
set -eo pipefail
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
EOS
InstallCommand = Struct.new(:name, :install_helm, :chart) do
def pod_name
"install-#{name}"
end
end
def initialize(kubeclient)
@kubeclient = kubeclient
@namespace = Gitlab::Kubernetes::Namespace.new(NAMESPACE, kubeclient)
end
def install(command)
@namespace.ensure_exists!
@kubeclient.create_pod(pod_resource(command))
end
##
# Returns Pod phase
#
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
#
# values: "Pending", "Running", "Succeeded", "Failed", "Unknown"
#
def installation_status(pod_name)
@kubeclient.get_pod(pod_name, @namespace.name).status.phase
end
def installation_log(pod_name)
@kubeclient.get_pod_log(pod_name, @namespace.name).body
end
def delete_installation_pod!(pod_name)
@kubeclient.delete_pod(pod_name, @namespace.name)
end
private
def pod_resource(command)
labels = { 'gitlab.org/action': 'install', 'gitlab.org/application': command.name }
metadata = { name: command.pod_name, namespace: @namespace.name, labels: labels }
container = {
name: 'helm',
image: 'alpine:3.6',
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
}
spec = { containers: [container], restartPolicy: 'Never' }
::Kubeclient::Resource.new(metadata: metadata, spec: spec)
end
def generate_pod_env(command)
{
HELM_VERSION: HELM_VERSION,
TILLER_NAMESPACE: @namespace.name,
COMMAND_SCRIPT: generate_script(command)
}.map { |key, value| { name: key, value: value } }
end
def generate_script(command)
[
INSTALL_DEPS,
helm_init_command(command),
helm_install_command(command)
].join("\n")
end
def helm_init_command(command)
if command.install_helm
'helm init >/dev/null'
else
'helm init --client-only >/dev/null'
end
end
def helm_install_command(command)
return if command.chart.nil?
"helm install #{command.chart} --name #{command.name} --namespace #{@namespace.name} >/dev/null"
end
end
end
end
module Gitlab
module Kubernetes
module Helm
class Api
def initialize(kubeclient)
@kubeclient = kubeclient
@namespace = Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, kubeclient)
end
def install(command)
@namespace.ensure_exists!
@kubeclient.create_pod(pod_resource(command))
end
##
# Returns Pod phase
#
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
#
# values: "Pending", "Running", "Succeeded", "Failed", "Unknown"
#
def installation_status(pod_name)
@kubeclient.get_pod(pod_name, @namespace.name).status.phase
end
def installation_log(pod_name)
@kubeclient.get_pod_log(pod_name, @namespace.name).body
end
def delete_installation_pod!(pod_name)
@kubeclient.delete_pod(pod_name, @namespace.name)
end
private
def pod_resource(command)
Pod.new(command, @namespace.name, @kubeclient).generate
end
end
end
end
end
module Gitlab
module Kubernetes
module Helm
class InstallCommand
attr_reader :name, :install_helm, :chart, :chart_values_file
def initialize(name, install_helm: false, chart: false, chart_values_file: false)
@name = name
@install_helm = install_helm
@chart = chart
@chart_values_file = chart_values_file
end
def pod_name
"install-#{name}"
end
def generate_script(namespace_name)
[
install_dps_command,
init_command,
complete_command(namespace_name)
].join("\n")
end
private
def init_command
if install_helm
'helm init >/dev/null'
else
'helm init --client-only >/dev/null'
end
end
def complete_command(namespace_name)
return unless chart
"helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null"
end
def install_dps_command
<<~HEREDOC
set -eo pipefail
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
HEREDOC
end
end
end
end
end
module Gitlab
module Kubernetes
module Helm
class Pod
def initialize(command, namespace_name, kubeclient)
@command = command
@namespace_name = namespace_name
@kubeclient = kubeclient
end
def generate
spec = { containers: [container_specification], restartPolicy: 'Never' }
if command.chart_values_file
generate_config_map
spec['volumes'] = volumes_specification
end
::Kubeclient::Resource.new(metadata: metadata, spec: spec)
end
private
attr_reader :command, :namespace_name, :kubeclient
def container_specification
container = {
name: 'helm',
image: 'alpine:3.6',
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
}
container[:volumeMounts] = volume_mounts_specification if command.chart_values_file
container
end
def labels
{ 'gitlab.org/action': 'install', 'gitlab.org/application': command.name }
end
def metadata
{ name: command.pod_name, namespace: namespace_name, labels: labels }
end
def volume_mounts_specification
[{ name: 'config-volume', mountPath: '/etc/config' }]
end
def volumes_specification
[{ name: 'config-volume', configMap: { name: 'values-config' } }]
end
def generate_pod_env(command)
{
HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION,
TILLER_NAMESPACE: namespace_name,
COMMAND_SCRIPT: command.generate_script(namespace_name)
}.map { |key, value| { name: key, value: value } }
end
def generate_config_map
resource = ::Kubeclient::Resource.new
resource.metadata = { name: 'values-config', namespace: namespace_name }
resource.data = YAML.load_file(command.chart_values_file)
kubeclient.create_config_map(resource)
end
end
end
end
end
......@@ -52,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do
context 'when application is already installing' do
before do
create(:cluster_applications_helm, :installing, cluster: cluster)
create(:clusters_applications_helm, :installing, cluster: cluster)
end
it 'returns 400' do
......
FactoryBot.define do
factory :cluster_applications_helm, class: Clusters::Applications::Helm do
factory :clusters_applications_helm, class: Clusters::Applications::Helm do
cluster factory: %i(cluster provided_by_gcp)
trait :not_installable do
......@@ -31,5 +31,8 @@ FactoryBot.define do
installing
updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
end
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
end
end
FactoryBot.define do
factory :cluster_applications_ingress, class: Clusters::Applications::Ingress do
cluster factory: %i(cluster provided_by_gcp)
trait :not_installable do
status(-2)
end
trait :installable do
status 0
end
trait :scheduled do
status 1
end
trait :installing do
status 2
end
trait :installed do
status 3
end
trait :errored do
status(-1)
status_reason 'something went wrong'
end
trait :timeouted do
installing
updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
end
end
end
......@@ -73,7 +73,7 @@ feature 'Clusters Applications', :js do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
create(:cluster_applications_helm, :installed, cluster: cluster)
create(:clusters_applications_helm, :installed, cluster: cluster)
page.within('.js-cluster-application-row-ingress') do
page.find(:css, '.js-cluster-application-install-button').click
......
......@@ -21,6 +21,7 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
});
......@@ -33,6 +34,10 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).toBeDefined();
});
it('renders a row for Prometheus', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined();
});
/* * /
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
......
......@@ -22,6 +22,11 @@ const CLUSTERS_MOCK_DATA = {
name: 'runner',
status: APPLICATION_INSTALLING,
status_reason: null,
},
{
name: 'prometheus',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
}],
},
},
......@@ -30,6 +35,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/helm': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
},
};
......
......@@ -82,6 +82,13 @@ describe('Clusters Store', () => {
requestStatus: null,
requestReason: null,
},
prometheus: {
title: 'Prometheus',
status: mockResponseData.applications[3].status,
statusReason: mockResponseData.applications[3].status_reason,
requestStatus: null,
requestReason: null,
},
},
});
});
......