summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/stylesheets/notify.scss6
-rw-r--r--app/models/clusters/applications/ingress.rb6
-rw-r--r--app/models/clusters/applications/prometheus.rb12
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/user_canonical_email.rb8
-rw-r--r--app/services/clusters/applications/check_upgrade_progress_service.rb71
-rw-r--r--app/services/clusters/applications/prometheus_config_service.rb155
-rw-r--r--app/services/clusters/applications/prometheus_update_service.rb35
-rw-r--r--app/services/clusters/applications/schedule_update_service.rb38
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/services/users/update_canonical_email_service.rb56
-rw-r--r--app/services/users/update_service.rb7
-rw-r--r--app/workers/all_queues.yml14
-rw-r--r--app/workers/cluster_update_app_worker.rb50
-rw-r--r--app/workers/cluster_wait_for_app_update_worker.rb16
16 files changed, 477 insertions, 6 deletions
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index ea82ba3e879..b59b01f4086 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -20,7 +20,11 @@ pre.commit-message {
}
.gl-label-scoped {
- box-shadow: 0 0 0 2px currentColor inset;
+ border: 2px solid currentColor;
+ box-sizing: border-box;
+ display: inline-block;
+ height: 17px;
+ line-height: 14px;
}
.gl-label-text {
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 78c2a74da33..baf34e916f8 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -50,7 +50,7 @@ module Clusters
end
def allowed_to_uninstall?
- external_ip_or_hostname? && application_jupyter_nil_or_installable?
+ external_ip_or_hostname? && !application_jupyter_installed?
end
def install_command
@@ -161,8 +161,8 @@ module Clusters
YAML.load_file(chart_values_file).deep_merge!(specification)
end
- def application_jupyter_nil_or_installable?
- cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
+ def application_jupyter_installed?
+ cluster.application_jupyter&.installed?
end
def modsecurity_snippet_content
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 8297f653ea7..3183318690c 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -35,6 +35,16 @@ module Clusters
.perform_async(application.cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
end
end
+
+ after_transition any => :updating do |application|
+ application.update(last_update_started_at: Time.now)
+ end
+ end
+
+ def updated_since?(timestamp)
+ last_update_started_at &&
+ last_update_started_at > timestamp &&
+ !update_errored?
end
def chart
@@ -148,5 +158,3 @@ module Clusters
end
end
end
-
-Clusters::Applications::Prometheus.prepend_if_ee('EE::Clusters::Applications::Prometheus')
diff --git a/app/models/project.rb b/app/models/project.rb
index 34c9c7320be..b9d8fd1e4d8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2411,6 +2411,12 @@ class Project < ApplicationRecord
branch_protection.fully_protected? || branch_protection.developer_can_merge?
end
+ def environments_for_scope(scope)
+ quoted_scope = ::Gitlab::SQL::Glob.q(scope)
+
+ environments.where("name LIKE (#{::Gitlab::SQL::Glob.to_like(quoted_scope)})") # rubocop:disable GitlabSecurity/SqlInjection
+ end
+
private
def closest_namespace_setting(name)
diff --git a/app/models/user.rb b/app/models/user.rb
index 7789326e8fa..4f484657f13 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -168,6 +168,7 @@ class User < ApplicationRecord
has_one :user_preference
has_one :user_detail
has_one :user_highest_role
+ has_one :user_canonical_email
#
# Validations
diff --git a/app/models/user_canonical_email.rb b/app/models/user_canonical_email.rb
new file mode 100644
index 00000000000..044e4fd775e
--- /dev/null
+++ b/app/models/user_canonical_email.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class UserCanonicalEmail < ApplicationRecord
+ validates :canonical_email, presence: true
+ validates :canonical_email, format: { with: Devise.email_regexp }
+
+ belongs_to :user, inverse_of: :user_canonical_email
+end
diff --git a/app/services/clusters/applications/check_upgrade_progress_service.rb b/app/services/clusters/applications/check_upgrade_progress_service.rb
new file mode 100644
index 00000000000..8502ea69f27
--- /dev/null
+++ b/app/services/clusters/applications/check_upgrade_progress_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class CheckUpgradeProgressService < BaseHelmService
+ def execute
+ return unless app.updating?
+
+ case phase
+ when ::Gitlab::Kubernetes::Pod::SUCCEEDED
+ on_success
+ when ::Gitlab::Kubernetes::Pod::FAILED
+ on_failed
+ else
+ check_timeout
+ end
+ rescue ::Kubeclient::HttpError => e
+ app.make_update_errored!("Kubernetes error: #{e.message}") unless app.update_errored?
+ end
+
+ private
+
+ def on_success
+ app.make_installed!
+ ensure
+ remove_pod
+ end
+
+ def on_failed
+ app.make_update_errored!(errors || 'Update silently failed')
+ ensure
+ remove_pod
+ end
+
+ def check_timeout
+ if timed_out?
+ begin
+ app.make_update_errored!('Update timed out')
+ ensure
+ remove_pod
+ end
+ else
+ ::ClusterWaitForAppUpdateWorker.perform_in(
+ ::ClusterWaitForAppUpdateWorker::INTERVAL, app.name, app.id)
+ end
+ end
+
+ def timed_out?
+ Time.now.utc - app.updated_at.to_time.utc > ::ClusterWaitForAppUpdateWorker::TIMEOUT
+ end
+
+ def remove_pod
+ helm_api.delete_pod!(pod_name)
+ rescue
+ # no-op
+ end
+
+ def phase
+ helm_api.status(pod_name)
+ end
+
+ def errors
+ helm_api.log(pod_name)
+ end
+
+ def pod_name
+ @pod_name ||= patch_command.pod_name
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/prometheus_config_service.rb b/app/services/clusters/applications/prometheus_config_service.rb
new file mode 100644
index 00000000000..34d44ab881e
--- /dev/null
+++ b/app/services/clusters/applications/prometheus_config_service.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class PrometheusConfigService
+ def initialize(project, cluster, app)
+ @project = project
+ @cluster = cluster
+ @app = app
+ end
+
+ def execute(config = {})
+ if has_alerts?
+ generate_alert_manager(config)
+ else
+ reset_alert_manager(config)
+ end
+ end
+
+ private
+
+ attr_reader :project, :cluster, :app
+
+ def reset_alert_manager(config)
+ config = set_alert_manager_enabled(config, false)
+ config.delete('alertmanagerFiles')
+ config['serverFiles'] ||= {}
+ config['serverFiles']['alerts'] = {}
+
+ config
+ end
+
+ def generate_alert_manager(config)
+ config = set_alert_manager_enabled(config, true)
+ config = set_alert_manager_files(config)
+
+ set_alert_manager_groups(config)
+ end
+
+ def set_alert_manager_enabled(config, enabled)
+ config['alertmanager'] ||= {}
+ config['alertmanager']['enabled'] = enabled
+
+ config
+ end
+
+ def set_alert_manager_files(config)
+ config['alertmanagerFiles'] = {
+ 'alertmanager.yml' => {
+ 'receivers' => alert_manager_receivers_params,
+ 'route' => alert_manager_route_params
+ }
+ }
+
+ config
+ end
+
+ def set_alert_manager_groups(config)
+ config['serverFiles'] ||= {}
+ config['serverFiles']['alerts'] ||= {}
+ config['serverFiles']['alerts']['groups'] ||= []
+
+ environments_with_alerts.each do |env_name, alerts|
+ index = config['serverFiles']['alerts']['groups'].find_index do |group|
+ group['name'] == env_name
+ end
+
+ if index
+ config['serverFiles']['alerts']['groups'][index]['rules'] = alerts
+ else
+ config['serverFiles']['alerts']['groups'] << {
+ 'name' => env_name,
+ 'rules' => alerts
+ }
+ end
+ end
+
+ config
+ end
+
+ def alert_manager_receivers_params
+ [
+ {
+ 'name' => 'gitlab',
+ 'webhook_configs' => [
+ {
+ 'url' => notify_url,
+ 'send_resolved' => true,
+ 'http_config' => {
+ 'bearer_token' => alert_manager_token
+ }
+ }
+ ]
+ }
+ ]
+ end
+
+ def alert_manager_token
+ app.generate_alert_manager_token!
+
+ app.alert_manager_token
+ end
+
+ def alert_manager_route_params
+ {
+ 'receiver' => 'gitlab',
+ 'group_wait' => '30s',
+ 'group_interval' => '5m',
+ 'repeat_interval' => '4h'
+ }
+ end
+
+ def notify_url
+ ::Gitlab::Routing.url_helpers
+ .notify_project_prometheus_alerts_url(project, format: :json)
+ end
+
+ def has_alerts?
+ environments_with_alerts.values.flatten(1).any?
+ end
+
+ def environments_with_alerts
+ @environments_with_alerts ||=
+ environments.each_with_object({}) do |environment, hash|
+ name = rule_name(environment)
+ hash[name] = alerts(environment)
+ end
+ end
+
+ def rule_name(environment)
+ "#{environment.name}.rules"
+ end
+
+ def alerts(environment)
+ variables = Gitlab::Prometheus::QueryVariables.call(environment)
+ alerts = Projects::Prometheus::AlertsFinder
+ .new(environment: environment)
+ .execute
+
+ alerts.map do |alert|
+ substitute_query_variables(alert.to_param, variables)
+ end
+ end
+
+ def substitute_query_variables(hash, variables)
+ hash['expr'] %= variables
+ hash
+ end
+
+ def environments
+ project.environments_for_scope(cluster.environment_scope)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/prometheus_update_service.rb b/app/services/clusters/applications/prometheus_update_service.rb
new file mode 100644
index 00000000000..437f6ab1202
--- /dev/null
+++ b/app/services/clusters/applications/prometheus_update_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class PrometheusUpdateService < BaseHelmService
+ attr_accessor :project
+
+ def initialize(app, project)
+ super(app)
+ @project = project
+ end
+
+ def execute
+ app.make_updating!
+
+ helm_api.update(patch_command(values))
+
+ ::ClusterWaitForAppUpdateWorker.perform_in(::ClusterWaitForAppUpdateWorker::INTERVAL, app.name, app.id)
+ rescue ::Kubeclient::HttpError => ke
+ app.make_update_errored!("Kubernetes error: #{ke.message}")
+ rescue StandardError => e
+ app.make_update_errored!(e.message)
+ end
+
+ private
+
+ def values
+ PrometheusConfigService
+ .new(project, cluster, app)
+ .execute
+ .to_yaml
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/schedule_update_service.rb b/app/services/clusters/applications/schedule_update_service.rb
new file mode 100644
index 00000000000..b7639c771a8
--- /dev/null
+++ b/app/services/clusters/applications/schedule_update_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class ScheduleUpdateService
+ BACKOFF_DELAY = 2.minutes
+
+ attr_accessor :application, :project
+
+ def initialize(application, project)
+ @application = application
+ @project = project
+ end
+
+ def execute
+ return unless application
+
+ if recently_scheduled?
+ worker_class.perform_in(BACKOFF_DELAY, application.name, application.id, project.id, Time.now)
+ else
+ worker_class.perform_async(application.name, application.id, project.id, Time.now)
+ end
+ end
+
+ private
+
+ def worker_class
+ ::ClusterUpdateAppWorker
+ end
+
+ def recently_scheduled?
+ return false unless application.last_update_started_at
+
+ application.last_update_started_at.utc >= Time.now.utc - BACKOFF_DELAY
+ end
+ end
+ end
+end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 4c3ae2d204d..6f9f307c322 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -30,6 +30,8 @@ module Users
build_identity(user)
+ Users::UpdateCanonicalEmailService.new(user: user).execute
+
user
end
diff --git a/app/services/users/update_canonical_email_service.rb b/app/services/users/update_canonical_email_service.rb
new file mode 100644
index 00000000000..1400fd58eb4
--- /dev/null
+++ b/app/services/users/update_canonical_email_service.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Users
+ class UpdateCanonicalEmailService
+ extend ActiveSupport::Concern
+
+ INCLUDED_DOMAINS_PATTERN = [/gmail.com/].freeze
+
+ def initialize(user:)
+ raise ArgumentError.new("Please provide a user") unless user&.is_a?(User)
+
+ @user = user
+ end
+
+ def execute
+ return unless user.email
+ return unless user.email.match? Devise.email_regexp
+
+ canonical_email = canonicalize_email
+
+ unless canonical_email
+ # the canonical email doesn't exist, probably because the domain doesn't match
+ # destroy any UserCanonicalEmail record associated with this user
+ user.user_canonical_email&.delete
+ # nothing else to do here
+ return
+ end
+
+ if user.user_canonical_email
+ # update to the new value
+ user.user_canonical_email.canonical_email = canonical_email
+ else
+ user.build_user_canonical_email(canonical_email: canonical_email)
+ end
+ end
+
+ private
+
+ attr_reader :user
+
+ def canonicalize_email
+ email = user.email
+
+ portions = email.split('@')
+ username = portions.shift
+ rest = portions.join
+
+ regex = Regexp.union(INCLUDED_DOMAINS_PATTERN)
+ return unless regex.match?(rest)
+
+ no_dots = username.tr('.', '')
+ before_plus = no_dots.split('+')[0]
+ "#{before_plus}@#{rest}"
+ end
+ end
+end
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index 57209043e3b..f0e9f2b7656 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -21,6 +21,7 @@ module Users
discard_read_only_attributes
assign_attributes
assign_identity
+ build_canonical_email
if @user.save(validate: validate) && update_status
notify_success(user_exists)
@@ -40,6 +41,12 @@ module Users
private
+ def build_canonical_email
+ return unless @user.email_changed?
+
+ Users::UpdateCanonicalEmailService.new(user: @user).execute
+ end
+
def update_status
return true unless @status_params
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f4d8483db84..cae4bb73e04 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -325,6 +325,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
+- :name: gcp_cluster:cluster_update_app
+ :feature_category: :kubernetes_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
- :name: gcp_cluster:cluster_upgrade_app
:feature_category: :kubernetes_management
:has_external_dependencies: true
@@ -339,6 +346,13 @@
:resource_boundary: :cpu
:weight: 1
:idempotent:
+- :name: gcp_cluster:cluster_wait_for_app_update
+ :feature_category: :kubernetes_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
- :name: gcp_cluster:cluster_wait_for_ingress_ip_address
:feature_category: :kubernetes_management
:has_external_dependencies: true
diff --git a/app/workers/cluster_update_app_worker.rb b/app/workers/cluster_update_app_worker.rb
new file mode 100644
index 00000000000..7ceeb167b33
--- /dev/null
+++ b/app/workers/cluster_update_app_worker.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class ClusterUpdateAppWorker # rubocop:disable Scalability/IdempotentWorker
+ UpdateAlreadyInProgressError = Class.new(StandardError)
+
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+ include ExclusiveLeaseGuard
+
+ sidekiq_options retry: 3, dead: false
+
+ LEASE_TIMEOUT = 10.minutes.to_i
+
+ def perform(app_name, app_id, project_id, scheduled_time)
+ @app_id = app_id
+
+ try_obtain_lease do
+ execute(app_name, app_id, project_id, scheduled_time)
+ end
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute(app_name, app_id, project_id, scheduled_time)
+ project = Project.find_by(id: project_id)
+ return unless project
+
+ find_application(app_name, app_id) do |app|
+ update_prometheus(app, scheduled_time, project)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def update_prometheus(app, scheduled_time, project)
+ return if app.updated_since?(scheduled_time)
+ return if app.update_in_progress?
+
+ Clusters::Applications::PrometheusUpdateService.new(app, project).execute
+ end
+
+ def lease_key
+ @lease_key ||= "#{self.class.name.underscore}-#{@app_id}"
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+end
diff --git a/app/workers/cluster_wait_for_app_update_worker.rb b/app/workers/cluster_wait_for_app_update_worker.rb
new file mode 100644
index 00000000000..9f1d83c2c7b
--- /dev/null
+++ b/app/workers/cluster_wait_for_app_update_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class ClusterWaitForAppUpdateWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ ::Clusters::Applications::CheckUpgradeProgressService.new(app).execute
+ end
+ end
+end