Extend Cluster Applications to allow installation of Prometheus

This commit is contained in:
Mayra Cabrera 2017-12-22 17:23:43 +00:00 committed by Kamil Trzciński
parent 79cbfedf67
commit 0d4548026f
39 changed files with 805 additions and 327 deletions

View file

@ -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);

View file

@ -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 -->

View file

@ -7,6 +7,7 @@ export default class ClusterService {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
};
}

View file

@ -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,
},
},
};
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,26 @@
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

View file

@ -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

View file

@ -0,0 +1,29 @@
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

View file

@ -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

View file

@ -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,

View file

@ -0,0 +1,5 @@
---
title: Add Prometheus to available Cluster applications
merge_request: 15895
author:
type: added

View file

@ -0,0 +1,18 @@
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

View file

@ -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

View file

@ -1,96 +1,8 @@
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

View file

@ -0,0 +1,42 @@
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

View file

@ -0,0 +1,53 @@
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

View file

@ -0,0 +1,69 @@
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

View file

@ -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

View file

@ -1,5 +1,5 @@
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

View file

@ -1,35 +0,0 @@
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

View file

@ -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

View file

@ -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();

View file

@ -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': { },
},
};

View file

@ -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,
},
},
});
});

View file

@ -1,22 +1,23 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm do
describe Gitlab::Kubernetes::Helm::Api do
let(:client) { double('kubernetes client') }
let(:helm) { described_class.new(client) }
let(:namespace) { Gitlab::Kubernetes::Namespace.new(described_class::NAMESPACE, client) }
let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) }
let(:install_helm) { true }
let(:chart) { 'stable/a_chart' }
let(:application_name) { 'app_name' }
let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm, chart) }
let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm: install_helm, chart: chart) }
subject { helm }
before do
allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client).and_return(namespace)
allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client).and_return(namespace)
end
describe '#initialize' do
it 'creates a namespace object' do
expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(described_class::NAMESPACE, client)
expect(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client)
subject
end
@ -41,7 +42,7 @@ describe Gitlab::Kubernetes::Helm do
let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation
it 'fetches POD phase from kubernetes cluster' do
expect(client).to receive(:get_pod).with(command.pod_name, described_class::NAMESPACE).once.and_return(pod)
expect(client).to receive(:get_pod).with(command.pod_name, gitlab_namespace).once.and_return(pod)
expect(subject.installation_status(command.pod_name)).to eq(phase)
end
@ -52,7 +53,7 @@ describe Gitlab::Kubernetes::Helm do
let(:response) { RestClient::Response.new(log) }
it 'fetches POD phase from kubernetes cluster' do
expect(client).to receive(:get_pod_log).with(command.pod_name, described_class::NAMESPACE).once.and_return(response)
expect(client).to receive(:get_pod_log).with(command.pod_name, gitlab_namespace).once.and_return(response)
expect(subject.installation_log(command.pod_name)).to eq(log)
end
@ -60,41 +61,9 @@ describe Gitlab::Kubernetes::Helm do
describe '#delete_installation_pod!' do
it 'deletes the POD from kubernetes cluster' do
expect(client).to receive(:delete_pod).with(command.pod_name, described_class::NAMESPACE).once
expect(client).to receive(:delete_pod).with(command.pod_name, gitlab_namespace).once
subject.delete_installation_pod!(command.pod_name)
end
end
describe '#helm_init_command' do
subject { helm.send(:helm_init_command, command) }
context 'when command.install_helm is true' do
let(:install_helm) { true }
it { is_expected.to eq('helm init >/dev/null') }
end
context 'when command.install_helm is false' do
let(:install_helm) { false }
it { is_expected.to eq('helm init --client-only >/dev/null') }
end
end
describe '#helm_install_command' do
subject { helm.send(:helm_install_command, command) }
context 'when command.chart is nil' do
let(:chart) { nil }
it { is_expected.to be_nil }
end
context 'when command.chart is set' do
let(:chart) { 'stable/a_chart' }
it { is_expected.to eq("helm install #{chart} --name #{application_name} --namespace #{namespace.name} >/dev/null")}
end
end
end

View file

@ -0,0 +1,111 @@
require 'rails_helper'
describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:prometheus) { create(:clusters_applications_prometheus) }
describe "#initialize" do
context "With all the params" do
subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) }
it 'should assign all parameters' do
expect(subject.name).to eq(prometheus.name)
expect(subject.install_helm).to be_truthy
expect(subject.chart).to eq(prometheus.chart)
expect(subject.chart_values_file).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
end
end
context 'when install_helm is not set' do
subject { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: true) }
it 'should set install_helm as false' do
expect(subject.install_helm).to be_falsy
end
end
context 'when chart is not set' do
subject { described_class.new(prometheus.name, install_helm: true) }
it 'should set chart as nil' do
expect(subject.chart).to be_falsy
end
end
context 'when chart_values_file is not set' do
subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart) }
it 'should set chart_values_file as nil' do
expect(subject.chart_values_file).to be_falsy
end
end
end
describe "#generate_script" do
let(:install_command) { described_class.new(prometheus.name, install_helm: install_helm) }
let(:client) { double('kubernetes client') }
let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) }
subject { install_command.send(:generate_script, namespace.name) }
context 'when install helm is true' do
let(:install_helm) { true }
let(:command) do
<<~MSG
set -eo pipefail
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
helm init >/dev/null
MSG
end
it 'should return appropriate command' do
is_expected.to eq(command)
end
end
context 'when install helm is false' do
let(:install_helm) { false }
let(:command) do
<<~MSG
set -eo pipefail
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
helm init --client-only >/dev/null
MSG
end
it 'should return appropriate command' do
is_expected.to eq(command)
end
end
context 'when chart is present' do
let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart) }
let(:command) do
<<~MSG.chomp
set -eo pipefail
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
helm init --client-only >/dev/null
helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} >/dev/null
MSG
end
it 'should return appropriate command' do
is_expected.to eq(command)
end
end
end
describe "#pod_name" do
let(:install_command) { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: true) }
subject { install_command.send(:pod_name) }
it { is_expected.to eq('install-prometheus') }
end
end

View file

@ -0,0 +1,86 @@
require 'rails_helper'
describe Gitlab::Kubernetes::Helm::Pod do
describe '#generate' do
let(:cluster) { create(:cluster) }
let(:app) { create(:clusters_applications_prometheus, cluster: cluster) }
let(:command) { app.install_command }
let(:client) { double('kubernetes client') }
let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) }
subject { described_class.new(command, namespace.name, client) }
before do
allow(client).to receive(:create_config_map).and_return(nil)
end
shared_examples 'helm pod' do
it 'should generate a Kubeclient::Resource' do
expect(subject.generate).to be_a_kind_of(Kubeclient::Resource)
end
it 'should generate the appropriate metadata' do
metadata = subject.generate.metadata
expect(metadata.name).to eq("install-#{app.name}")
expect(metadata.namespace).to eq('gitlab-managed-apps')
expect(metadata.labels['gitlab.org/action']).to eq('install')
expect(metadata.labels['gitlab.org/application']).to eq(app.name)
end
it 'should generate a container spec' do
spec = subject.generate.spec
expect(spec.containers.count).to eq(1)
end
it 'should generate the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
expect(container.image).to eq('alpine:3.6')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"])
end
it 'should include a never restart policy' do
spec = subject.generate.spec
expect(spec.restartPolicy).to eq('Never')
end
end
context 'with a configuration file' do
it_behaves_like 'helm pod'
it 'should include volumes for the container' do
container = subject.generate.spec.containers.first
expect(container.volumeMounts.first['name']).to eq('config-volume')
expect(container.volumeMounts.first['mountPath']).to eq('/etc/config')
end
it 'should include a volume inside the specification' do
spec = subject.generate.spec
expect(spec.volumes.first['name']).to eq('config-volume')
end
it 'should mount configMap specification in the volume' do
spec = subject.generate.spec
expect(spec.volumes.first.configMap['name']).to eq('values-config')
end
end
context 'without a configuration file' do
let(:app) { create(:clusters_applications_ingress, cluster: cluster) }
it_behaves_like 'helm pod'
it 'should not include volumeMounts inside the container' do
container = subject.generate.spec.containers.first
expect(container.volumeMounts).to be_nil
end
it 'should not a volume inside the specification' do
spec = subject.generate.spec
expect(spec.volumes).to be_nil
end
end
end
end

View file

@ -40,13 +40,13 @@ describe Clusters::Applications::Helm do
describe '#install_command' do
it 'has all the needed information' do
expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true, chart: nil)
expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true)
end
end
describe 'status state machine' do
describe '#make_installing' do
subject { create(:cluster_applications_helm, :scheduled) }
subject { create(:clusters_applications_helm, :scheduled) }
it 'is installing' do
subject.make_installing!
@ -56,7 +56,7 @@ describe Clusters::Applications::Helm do
end
describe '#make_installed' do
subject { create(:cluster_applications_helm, :installing) }
subject { create(:clusters_applications_helm, :installing) }
it 'is installed' do
subject.make_installed
@ -66,7 +66,7 @@ describe Clusters::Applications::Helm do
end
describe '#make_errored' do
subject { create(:cluster_applications_helm, :installing) }
subject { create(:clusters_applications_helm, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
@ -78,7 +78,7 @@ describe Clusters::Applications::Helm do
end
describe '#make_scheduled' do
subject { create(:cluster_applications_helm, :installable) }
subject { create(:clusters_applications_helm, :installable) }
it 'is scheduled' do
subject.make_scheduled
@ -87,7 +87,7 @@ describe Clusters::Applications::Helm do
end
describe 'when was errored' do
subject { create(:cluster_applications_helm, :errored) }
subject { create(:clusters_applications_helm, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil

View file

@ -4,105 +4,5 @@ describe Clusters::Applications::Ingress do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
describe '#name' do
it 'is .application_name' do
expect(subject.name).to eq(described_class.application_name)
end
it 'is recorded in Clusters::Cluster::APPLICATIONS' do
expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
end
end
describe '#status' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
subject { described_class.new(cluster: cluster) }
it 'defaults to :not_installable' do
expect(subject.status_name).to be(:not_installable)
end
context 'when application helm is scheduled' do
before do
create(:cluster_applications_helm, :scheduled, cluster: cluster)
end
it 'defaults to :not_installable' do
expect(subject.status_name).to be(:not_installable)
end
end
context 'when application helm is installed' do
before do
create(:cluster_applications_helm, :installed, cluster: cluster)
end
it 'defaults to :installable' do
expect(subject.status_name).to be(:installable)
end
end
end
describe '#install_command' do
it 'has all the needed information' do
expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false, chart: subject.chart)
end
end
describe 'status state machine' do
describe '#make_installing' do
subject { create(:cluster_applications_ingress, :scheduled) }
it 'is installing' do
subject.make_installing!
expect(subject).to be_installing
end
end
describe '#make_installed' do
subject { create(:cluster_applications_ingress, :installing) }
it 'is installed' do
subject.make_installed
expect(subject).to be_installed
end
end
describe '#make_errored' do
subject { create(:cluster_applications_ingress, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
subject.make_errored(reason)
expect(subject).to be_errored
expect(subject.status_reason).to eq(reason)
end
end
describe '#make_scheduled' do
subject { create(:cluster_applications_ingress, :installable) }
it 'is scheduled' do
subject.make_scheduled
expect(subject).to be_scheduled
end
describe 'when was errored' do
subject { create(:cluster_applications_ingress, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
subject.make_scheduled!
expect(subject.status_reason).to be_nil
end
end
end
end
include_examples 'cluster application specs', described_class
end

View file

@ -0,0 +1,16 @@
require 'rails_helper'
describe Clusters::Applications::Prometheus do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
include_examples 'cluster application specs', described_class
describe "#chart_values_file" do
subject { create(:clusters_applications_prometheus).chart_values_file }
it 'should return chart values file path' do
expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
end
end
end

View file

@ -5,6 +5,9 @@ describe Clusters::Cluster do
it { is_expected.to have_many(:projects) }
it { is_expected.to have_one(:provider_gcp) }
it { is_expected.to have_one(:platform_kubernetes) }
it { is_expected.to have_one(:application_helm) }
it { is_expected.to have_one(:application_ingress) }
it { is_expected.to have_one(:application_prometheus) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:status_name).to(:provider) }
@ -190,11 +193,12 @@ describe Clusters::Cluster do
end
context 'when applications are created' do
let!(:helm) { create(:cluster_applications_helm, cluster: cluster) }
let!(:ingress) { create(:cluster_applications_ingress, cluster: cluster) }
let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
it 'returns a list of created applications' do
is_expected.to contain_exactly(helm, ingress)
is_expected.to contain_exactly(helm, ingress, prometheus)
end
end
end

View file

@ -2,7 +2,7 @@ require 'spec_helper'
describe ClusterApplicationEntity do
describe '#as_json' do
let(:application) { build(:cluster_applications_helm) }
let(:application) { build(:clusters_applications_helm) }
subject { described_class.new(application).as_json }
it 'has name' do
@ -18,7 +18,7 @@ describe ClusterApplicationEntity do
end
context 'when application is errored' do
let(:application) { build(:cluster_applications_helm, :errored) }
let(:application) { build(:clusters_applications_helm, :errored) }
it 'has corresponded data' do
expect(subject[:status]).to eq(:errored)

View file

@ -3,7 +3,7 @@ require 'spec_helper'
describe Clusters::Applications::CheckInstallationProgressService do
RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
let(:application) { create(:cluster_applications_helm, :installing) }
let(:application) { create(:clusters_applications_helm, :installing) }
let(:service) { described_class.new(application) }
let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
let(:errors) { nil }
@ -33,7 +33,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
end
context 'when timeouted' do
let(:application) { create(:cluster_applications_helm, :timeouted) }
let(:application) { create(:clusters_applications_helm, :timeouted) }
it_behaves_like 'a terminated installation'

View file

@ -2,17 +2,19 @@ require 'spec_helper'
describe Clusters::Applications::InstallService do
describe '#execute' do
let(:application) { create(:cluster_applications_helm, :scheduled) }
let(:application) { create(:clusters_applications_helm, :scheduled) }
let!(:install_command) { application.install_command }
let(:service) { described_class.new(application) }
let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm) }
let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
before do
allow(service).to receive(:install_command).and_return(install_command)
allow(service).to receive(:helm_api).and_return(helm_client)
end
context 'when there are no errors' do
before do
expect(helm_client).to receive(:install).with(application.install_command)
expect(helm_client).to receive(:install).with(install_command)
allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
end
@ -33,7 +35,7 @@ describe Clusters::Applications::InstallService do
context 'when k8s cluster communication fails' do
before do
error = KubeException.new(500, 'system failure', nil)
expect(helm_client).to receive(:install).with(application.install_command).and_raise(error)
expect(helm_client).to receive(:install).with(install_command).and_raise(error)
end
it 'make the application errored' do
@ -45,7 +47,7 @@ describe Clusters::Applications::InstallService do
end
context 'when application cannot be persisted' do
let(:application) { build(:cluster_applications_helm, :scheduled) }
let(:application) { build(:clusters_applications_helm, :scheduled) }
it 'make the application errored' do
expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid)

View file

@ -34,7 +34,7 @@ describe Clusters::Applications::ScheduleInstallationService do
end
context 'when installation is already in progress' do
let(:application) { create(:cluster_applications_helm, :installing) }
let(:application) { create(:clusters_applications_helm, :installing) }
let(:cluster) { application.cluster }
it_behaves_like 'a failing service'

View file

@ -0,0 +1,105 @@
shared_examples 'cluster application specs' do
let(:factory_name) { described_class.to_s.downcase.gsub("::", "_") }
describe '#name' do
it 'is .application_name' do
expect(subject.name).to eq(described_class.application_name)
end
it 'is recorded in Clusters::Cluster::APPLICATIONS' do
expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
end
end
describe '#status' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
subject { described_class.new(cluster: cluster) }
it 'defaults to :not_installable' do
expect(subject.status_name).to be(:not_installable)
end
context 'when application helm is scheduled' do
before do
create(factory_name, :scheduled, cluster: cluster)
end
it 'defaults to :not_installable' do
expect(subject.status_name).to be(:not_installable)
end
end
context 'when application helm is installed' do
before do
create(:clusters_applications_helm, :installed, cluster: cluster)
end
it 'defaults to :installable' do
expect(subject.status_name).to be(:installable)
end
end
end
describe '#install_command' do
it 'has all the needed information' do
expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false)
end
end
describe 'status state machine' do
describe '#make_installing' do
subject { create(factory_name, :scheduled) }
it 'is installing' do
subject.make_installing!
expect(subject).to be_installing
end
end
describe '#make_installed' do
subject { create(factory_name, :installing) }
it 'is installed' do
subject.make_installed
expect(subject).to be_installed
end
end
describe '#make_errored' do
subject { create(factory_name, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
subject.make_errored(reason)
expect(subject).to be_errored
expect(subject.status_reason).to eq(reason)
end
end
describe '#make_scheduled' do
subject { create(factory_name, :installable) }
it 'is scheduled' do
subject.make_scheduled
expect(subject).to be_scheduled
end
describe 'when was errored' do
subject { create(factory_name, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
subject.make_scheduled!
expect(subject.status_reason).to be_nil
end
end
end
end
end

134
vendor/prometheus/values.yaml vendored Normal file
View file

@ -0,0 +1,134 @@
alertmanager: |
enabled: false
kubeStateMetrics: |
enabled: 'false'
nodeExporter: |
enabled: 'false'
pushgateway: |
enabled: 'false'
serverFiles: |
alerts: ''
rules: ''
prometheus.yml: |-
rule_files: |
- /etc/config/rules
- /etc/config/alerts
scrape_configs: |
- job_name: prometheus
static_configs: |
- targets:
- localhost:9090
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs: |
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: default;kubernetes;https
- job_name: 'kubernetes-nodes'
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
kubernetes_sd_configs:
- role: node
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
- target_label: __address__
replacement: kubernetes.default.svc:443
- source_labels: [__meta_kubernetes_node_name]
regex: (.+)
target_label: __metrics_path__
replacement: /api/v1/nodes/${1}/proxy/metrics
- job_name: 'kubernetes-service-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs: |
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: 'true'
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: (.+)(?::\d+);(\d+)
replacement: $1:$2
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
action: replace
target_label: kubernetes_name
- job_name: 'prometheus-pushgateway'
honor_labels: true
kubernetes_sd_configs: |
- role: service
relabel_configs: |
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
action: keep
regex: pushgateway
- job_name: 'kubernetes-services'
metrics_path: /probe
params: |
module: [http_2xx]
kubernetes_sd_configs: |
- role: service
relabel_configs: |
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
action: keep
regex: 'true'
- source_labels: [__address__]
target_label: __param_target
- target_label: __address__
replacement: blackbox
- source_labels: [__param_target]
target_label: instance
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: 'true'
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name