summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/board_group_recent_visit.rb25
-rw-r--r--app/models/board_project_recent_visit.rb25
-rw-r--r--app/models/clusters/cluster.rb28
-rw-r--r--app/models/clusters/group.rb10
-rw-r--r--app/models/commit_status.rb5
-rw-r--r--app/models/concerns/deployment_platform.rb1
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/environment_status.rb41
-rw-r--r--app/models/global_milestone.rb44
-rw-r--r--app/models/group.rb3
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/models/milestone.rb18
-rw-r--r--app/models/ssh_host_key.rb130
-rw-r--r--app/models/user.rb2
15 files changed, 289 insertions, 55 deletions
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
new file mode 100644
index 00000000000..92abbb67222
--- /dev/null
+++ b/app/models/board_group_recent_visit.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Tracks which boards in a specific group a user has visited
+class BoardGroupRecentVisit < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :group
+ belongs_to :board
+
+ validates :user, presence: true
+ validates :group, presence: true
+ validates :board, presence: true
+
+ scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) }
+
+ def self.visited!(user, board)
+ visit = find_or_create_by(user: user, group: board.group, board: board)
+ visit.touch if visit.updated_at < Time.now
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def self.latest(user, group)
+ by_user_group(user, group).last
+ end
+end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
new file mode 100644
index 00000000000..7cffff906d8
--- /dev/null
+++ b/app/models/board_project_recent_visit.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Tracks which boards in a specific project a user has visited
+class BoardProjectRecentVisit < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+ belongs_to :board
+
+ validates :user, presence: true
+ validates :project, presence: true
+ validates :board, presence: true
+
+ scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) }
+
+ def self.visited!(user, board)
+ visit = find_or_create_by(user: user, project: board.project, board: board)
+ visit.touch if visit.updated_at < Time.now
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def self.latest(user, project)
+ by_user_project(user, project).last
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 95efecfc41d..222e4217e67 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -20,6 +20,12 @@ module Clusters
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
+ has_many :cluster_groups, class_name: 'Clusters::Group'
+ has_many :groups, through: :cluster_groups, class_name: '::Group'
+
+ has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group'
+ has_one :group, through: :cluster_group, class_name: '::Group'
+
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
@@ -38,8 +44,12 @@ module Clusters
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
+ validates :cluster_type, presence: true
validate :restrict_modification, on: :update
+ validate :no_groups, unless: :group_type?
+ validate :no_projects, unless: :project_type?
+
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
@@ -50,6 +60,12 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
+ enum cluster_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
+
enum platform_type: {
kubernetes: 1
}
@@ -122,5 +138,17 @@ module Clusters
true
end
+
+ def no_groups
+ if groups.any?
+ errors.add(:cluster, 'cannot have groups assigned')
+ end
+ end
+
+ def no_projects
+ if projects.any?
+ errors.add(:cluster, 'cannot have projects assigned')
+ end
+ end
end
end
diff --git a/app/models/clusters/group.rb b/app/models/clusters/group.rb
new file mode 100644
index 00000000000..2b08a9e47f0
--- /dev/null
+++ b/app/models/clusters/group.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Clusters
+ class Group < ActiveRecord::Base
+ self.table_name = 'cluster_groups'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :group, class_name: '::Group'
+ end
+end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 06507345fe8..7d36f9982ab 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -50,7 +50,8 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4,
missing_dependency_failure: 5,
runner_unsupported: 6,
- stale_schedule: 7
+ stale_schedule: 7,
+ job_execution_timeout: 8
}
##
@@ -109,7 +110,7 @@ class CommitStatus < ActiveRecord::Base
before_transition any => :failed do |commit_status, transition|
failure_reason = transition.args.first
- commit_status.failure_reason = failure_reason
+ commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason]
end
after_transition do |commit_status, transition|
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index 91052013592..e57a3383544 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -42,6 +42,7 @@ module DeploymentPlatform
{
name: 'kubernetes-template',
projects: [self],
+ cluster_type: :project_type,
provider_type: :user,
platform_type: :kubernetes,
platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 62dc0f2cbeb..ee5b96e7454 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base
metrics&.merge(deployment_time: created_at.to_i) || {}
end
+ def status
+ 'success'
+ end
+
private
def prometheus_adapter
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 0816c395185..1c31c01eb9f 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -149,7 +149,7 @@ class Environment < ActiveRecord::Base
end
def has_metrics?
- prometheus_adapter&.can_query? && available? && last_deployment.present?
+ prometheus_adapter&.can_query? && available?
end
def metrics
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 5ff3acc0e58..a84871f7253 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -3,21 +3,33 @@
class EnvironmentStatus
include Gitlab::Utils::StrongMemoize
- attr_reader :environment, :merge_request
+ attr_reader :environment, :merge_request, :sha
delegate :id, to: :environment
delegate :name, to: :environment
delegate :project, to: :environment
delegate :deployed_at, to: :deployment, allow_nil: true
+ delegate :status, to: :deployment
- def initialize(environment, merge_request)
+ def self.for_merge_request(mr, user)
+ build_environments_status(mr, user, mr.head_pipeline)
+ end
+
+ def self.after_merge_request(mr, user)
+ return [] unless mr.merged?
+
+ build_environments_status(mr, user, mr.merge_pipeline)
+ end
+
+ def initialize(environment, merge_request, sha)
@environment = environment
@merge_request = merge_request
+ @sha = sha
end
def deployment
strong_memoize(:deployment) do
- environment.first_deployment_for(merge_request.diff_head_sha)
+ environment.first_deployment_for(sha)
end
end
@@ -26,10 +38,9 @@ class EnvironmentStatus
end
def changes
- sha = merge_request.diff_head_sha
return [] if project.route_map_for(sha).nil?
- changed_files.map { |file| build_change(file, sha) }.compact
+ changed_files.map { |file| build_change(file) }.compact
end
def changed_files
@@ -41,7 +52,7 @@ class EnvironmentStatus
PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze
- def build_change(file, sha)
+ def build_change(file)
public_path = project.public_path_for_source_path(file.new_path, sha)
return if public_path.nil?
@@ -53,4 +64,22 @@ class EnvironmentStatus
external_url: environment.external_url_for(file.new_path, sha)
}
end
+
+ def self.build_environments_status(mr, user, pipeline)
+ return [] unless pipeline.present?
+
+ find_environments(user, pipeline).map do |environment|
+ EnvironmentStatus.new(environment, mr, pipeline.sha)
+ end
+ end
+ private_class_method :build_environments_status
+
+ def self.find_environments(user, pipeline)
+ env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id)
+
+ Environment.available.where(id: env_ids).select do |environment|
+ Ability.allowed?(user, :read_environment, environment)
+ end
+ end
+ private_class_method :find_environments
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index a6cebabe089..085ffd16c6a 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -34,50 +34,6 @@ class GlobalMilestone
new(title, child_milestones)
end
- def self.states_count(projects, group = nil)
- legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
- group_milestones_count = group_milestones_states_count(group)
-
- legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
- legacy_group_milestones_count + group_milestones_count
- end
- end
-
- def self.group_milestones_states_count(group)
- return STATE_COUNT_HASH unless group
-
- params = { group_ids: [group.id], state: 'all' }
-
- relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
- grouped_by_state = relation.reorder(nil).group(:state).count
-
- {
- opened: grouped_by_state['active'] || 0,
- closed: grouped_by_state['closed'] || 0,
- all: grouped_by_state.values.sum
- }
- end
-
- # Counts the legacy group milestones which must be grouped by title
- def self.legacy_group_milestone_states_count(projects)
- return STATE_COUNT_HASH unless projects
-
- params = { project_ids: projects.map(&:id), state: 'all' }
-
- relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
- project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
-
- opened = count_by_state(project_milestones_by_state_and_title, 'active')
- closed = count_by_state(project_milestones_by_state_and_title, 'closed')
- all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
-
- {
- opened: opened,
- closed: closed,
- all: all
- }
- end
-
def self.count_by_state(milestones_by_state_and_title, state)
milestones_by_state_and_title.count do |(milestone_state, _), _|
milestone_state == state
diff --git a/app/models/group.rb b/app/models/group.rb
index 612c546ca57..c67479110c9 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -41,6 +41,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
+ has_many :cluster_groups, class_name: 'Clusters::Group'
+ has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
+
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6559f94a696..7eef08aa6a3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end
+ def merge_pipeline
+ return unless merged?
+
+ target_project.pipeline_for(target_branch, merge_commit_sha)
+ end
+
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 892a680f221..3cc8e2c44bb 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -145,7 +145,7 @@ class Milestone < ActiveRecord::Base
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
+ User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct
end
def self.sort_by_attribute(method)
@@ -170,6 +170,22 @@ class Milestone < ActiveRecord::Base
sorted.with_order_id_desc
end
+ def self.states_count(projects, groups = nil)
+ return STATE_COUNT_HASH unless projects || groups
+
+ counts = Milestone
+ .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id))
+ .reorder(nil)
+ .group(:state)
+ .count
+
+ {
+ opened: counts['active'] || 0,
+ closed: counts['closed'] || 0,
+ all: counts.values.sum
+ }
+ end
+
##
# Returns the String necessary to reference this Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
new file mode 100644
index 00000000000..b6844dbe870
--- /dev/null
+++ b/app/models/ssh_host_key.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+# Detected SSH host keys are transiently stored in Redis
+class SshHostKey
+ class Fingerprint < Gitlab::SSHPublicKey
+ attr_reader :index
+
+ def initialize(key, index: nil)
+ super(key)
+
+ @index = index
+ end
+
+ def as_json(*)
+ { bits: bits, fingerprint: fingerprint, type: type, index: index }
+ end
+ end
+
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(key) { [key.class.to_s, key.id] }
+
+ # Do not refresh the data in the background - it is not expected to change.
+ # This is achieved by making the lifetime shorter than the refresh interval.
+ self.reactive_cache_refresh_interval = 15.minutes
+ self.reactive_cache_lifetime = 10.minutes
+
+ def self.find_by(opts = {})
+ return nil unless opts.key?(:id)
+
+ project_id, url = opts[:id].split(':', 2)
+ project = Project.find_by(id: project_id)
+
+ project.presence && new(project: project, url: url)
+ end
+
+ def self.fingerprint_host_keys(data)
+ return [] unless data.is_a?(String)
+
+ data
+ .each_line
+ .each_with_index
+ .map { |line, index| Fingerprint.new(line, index: index) }
+ .select(&:valid?)
+ end
+
+ attr_reader :project, :url, :compare_host_keys
+
+ def initialize(project:, url:, compare_host_keys: nil)
+ @project = project
+ @url = normalize_url(url)
+ @compare_host_keys = compare_host_keys
+ end
+
+ def id
+ [project.id, url].join(':')
+ end
+
+ def as_json(*)
+ {
+ host_keys_changed: host_keys_changed?,
+ fingerprints: fingerprints,
+ known_hosts: known_hosts
+ }
+ end
+
+ def known_hosts
+ with_reactive_cache { |data| data[:known_hosts] }
+ end
+
+ def fingerprints
+ @fingerprints ||= self.class.fingerprint_host_keys(known_hosts)
+ end
+
+ # Returns true if the known_hosts data differs from the version passed in at
+ # initialization as `compare_host_keys`. Comments, ordering, etc, is ignored
+ def host_keys_changed?
+ cleanup(known_hosts) != cleanup(compare_host_keys)
+ end
+
+ def error
+ with_reactive_cache { |data| data[:error] }
+ end
+
+ def calculate_reactive_cache
+ known_hosts, errors, status =
+ Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr|
+ stdin.puts(url.host)
+ stdin.close
+
+ [
+ cleanup(stdout.read),
+ cleanup(stderr.read),
+ wait_thr.value
+ ]
+ end
+
+ # ssh-keyscan returns an exit code 0 in several error conditions, such as an
+ # unknown hostname, so check both STDERR and the exit code
+ if status.success? && !errors.present?
+ { known_hosts: known_hosts }
+ else
+ Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}")
+
+ { error: 'Failed to detect SSH host keys' }
+ end
+ end
+
+ private
+
+ # Remove comments and duplicate entries
+ def cleanup(data)
+ data
+ .to_s
+ .each_line
+ .reject { |line| line.start_with?('#') || line.chomp.empty? }
+ .uniq
+ .sort
+ .join
+ end
+
+ def normalize_url(url)
+ full_url = ::Addressable::URI.parse(url)
+ raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh'
+
+ Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}")
+ rescue Addressable::URI::InvalidURIError
+ raise ArgumentError.new("Invalid URL")
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 986603dec96..cc2cd1b7723 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1138,7 +1138,7 @@ class User < ActiveRecord::Base
events = Event.select(:project_id)
.contributions.where(author_id: self)
.where("created_at > ?", Time.now - 1.year)
- .uniq
+ .distinct
.reorder(nil)
Project.where(id: events)