ee/app/models/geo_node.rb (276 lines of code) (raw):

# frozen_string_literal: true class GeoNode < ApplicationRecord include Presentable include Geo::SelectiveSync include StripAttribute SELECTIVE_SYNC_TYPES = %w[namespaces shards].freeze # Array of repository storages to synchronize for selective sync by shards serialize :selective_sync_shards, Array # rubocop:disable Cop/ActiveRecordSerialize belongs_to :oauth_application, class_name: 'Doorkeeper::Application', dependent: :destroy, autosave: true # rubocop: disable Cop/ActiveRecordDependent has_many :geo_node_namespace_links has_many :namespaces, through: :geo_node_namespace_links has_one :status, class_name: 'GeoNodeStatus' validates :name, presence: true, uniqueness: { case_sensitive: false }, length: { maximum: 255 } validates :url, presence: true, addressable_url: true validates :internal_url, addressable_url: true, allow_blank: true, allow_nil: true validates :primary, uniqueness: { message: 'node already exists' }, if: :primary validates :enabled, if: :primary, acceptance: { message: 'Geo primary node cannot be disabled' } validates :access_key, presence: true validates :encrypted_secret_access_key, presence: true validates :selective_sync_type, inclusion: { in: SELECTIVE_SYNC_TYPES, allow_blank: true, allow_nil: true } validates :repos_max_capacity, numericality: { greater_than_or_equal_to: 0 } validates :files_max_capacity, numericality: { greater_than_or_equal_to: 0 } validates :verification_max_capacity, numericality: { greater_than_or_equal_to: 0 } validates :container_repositories_max_capacity, numericality: { greater_than_or_equal_to: 0 } validates :minimum_reverification_interval, numericality: { greater_than_or_equal_to: 1 } validate :require_current_node_to_be_primary, if: :secondary? validate :require_hashed_storage, on: :create after_save :expire_cache! after_destroy :expire_cache! before_validation :update_dependents_attributes before_validation :ensure_access_keys! alias_method :repair, :save # the `update_dependents_attributes` hook will take care of it scope :with_url_prefix, ->(prefix) { where('url LIKE ?', "#{prefix}%") } scope :secondary_nodes, -> { where(primary: false) } attr_encrypted :secret_access_key, key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-gcm', mode: :per_attribute_iv, encode: true strip_attributes :name class << self # Set in gitlab.rb as external_url def current_node_url RequestStore.fetch('geo_node:current_node_url') do Gitlab.config.gitlab.url end end # Set in gitlab.rb as geo_node_name def current_node_name RequestStore.fetch('geo_node:current_node_name') do Gitlab.config.geo.node_name end end def current_node return unless column_names.include?('name') GeoNode.find_by(name: current_node_name) end def primary_node find_by(primary: true) end def unhealthy_nodes status_table = GeoNodeStatus.arel_table query = status_table[:id].eq(nil) .or(status_table[:cursor_last_event_id].eq(nil)) .or(status_table[:last_successful_status_check_at].eq(nil)) .or(status_table[:last_successful_status_check_at].lt(10.minutes.ago)) left_join_status.where(query) end def min_cursor_last_event_id left_join_status.minimum(:cursor_last_event_id) end # Tries to find a GeoNode by oauth_application_id, returning nil if none could be found. def find_by_oauth_application_id(oauth_application_id) where(oauth_application_id: oauth_application_id).take end private def left_join_status join_statement = arel_table.join(GeoNodeStatus.arel_table, Arel::Nodes::OuterJoin) .on(arel_table[:id].eq(GeoNodeStatus.arel_table[:geo_node_id])) joins(join_statement.join_sources) end end def current? self.class.current_node_name == name end def secondary? !primary end def uses_ssh_key? secondary? && clone_protocol == 'ssh' end def name value = read_attribute(:name) if looks_like_url_field_missing_slash?(value) add_ending_slash(value) else value end end def name=(value) if looks_like_url_field_missing_slash?(value) write_with_ending_slash(:name, value) else write_attribute(:name, value) end end def url read_with_ending_slash(:url) end def url=(value) write_with_ending_slash(:url, value) @uri = nil end def internal_url read_with_ending_slash(:internal_url).presence || read_with_ending_slash(:url) end def internal_url=(value) value = add_ending_slash(value) != url ? value : nil write_with_ending_slash(:internal_url, value) @internal_uri = nil end def uri @uri ||= URI.parse(url) if url.present? end def internal_uri @internal_uri ||= URI.parse(internal_url) if internal_url.present? end def geo_transfers_url(file_type, file_id) geo_api_url("transfers/#{file_type}/#{file_id}") end def status_url geo_api_url('status') end def snapshot_url(repository) url = api_url("projects/#{repository.project.id}/snapshot") url += "?wiki=1" if repository.repo_type.wiki? url end def oauth_callback_url Gitlab::Routing.url_helpers.oauth_geo_callback_url(url_helper_args) end def oauth_logout_url(state) Gitlab::Routing.url_helpers.oauth_geo_logout_url(url_helper_args.merge(state: state)) end def geo_projects_url return unless self.secondary? Gitlab::Routing.url_helpers.admin_geo_projects_url(url_helper_args) end def missing_oauth_application? self.primary? ? false : !oauth_application.present? end def update_clone_url! update_clone_url # Update with update_column to prevent calling callbacks as this method will # be called in an initializer and we don't want other callbacks # to mess with uninitialized dependencies. if clone_url_prefix_changed? Rails.logger.info "Geo: modified clone_url_prefix to #{clone_url_prefix}" # rubocop:disable Gitlab/RailsLogger update_column(:clone_url_prefix, clone_url_prefix) end end def job_artifacts Ci::JobArtifact.all unless selective_sync? Ci::JobArtifact.project_id_in(projects) end def container_repositories return ContainerRepository.all unless selective_sync? ContainerRepository.project_id_in(projects) end def lfs_objects return LfsObject.all unless selective_sync? LfsObject.project_id_in(projects) end def projects return Project.all unless selective_sync? if selective_sync_by_namespaces? projects_for_selected_namespaces elsif selective_sync_by_shards? projects_for_selected_shards else Project.none end end def projects_include?(project_id) return true unless selective_sync? projects.where(id: project_id).exists? end def replication_slots_count return unless Gitlab::Database.replication_slots_supported? && primary? PgReplicationSlot.count end def replication_slots_used_count return unless Gitlab::Database.replication_slots_supported? && primary? PgReplicationSlot.used_slots_count end def replication_slots_max_retained_wal_bytes return unless Gitlab::Database.replication_slots_supported? && primary? PgReplicationSlot.max_retained_wal end def find_or_build_status status || build_status end private def geo_api_url(suffix) api_url("geo/#{suffix}") end def api_url(suffix) Gitlab::Utils.append_path(internal_uri.to_s, "api/#{API::API.version}/#{suffix}") end def ensure_access_keys! return if self.access_key.present? && self.encrypted_secret_access_key.present? keys = Gitlab::Geo.generate_access_keys self.access_key = keys[:access_key] self.secret_access_key = keys[:secret_access_key] end def url_helper_args url_helper_options(uri) end def url_helper_options(given_uri) { protocol: given_uri.scheme, host: given_uri.host, port: given_uri.port, script_name: given_uri.path } end def update_dependents_attributes if self.primary? self.oauth_application = nil update_clone_url else update_oauth_application! end end # Prevent locking yourself out def require_current_node_to_be_primary if name == self.class.current_node_name errors.add(:base, 'Current node must be the primary node or you will be locking yourself out') end end # Prevent creating a Geo Node unless Hashed Storage is enabled def require_hashed_storage unless Gitlab::CurrentSettings.hashed_storage_enabled? errors.add(:base, 'Hashed Storage must be enabled to use Geo') end end def update_clone_url self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix end def update_oauth_application! return unless uri self.build_oauth_application if oauth_application.nil? self.oauth_application.name = "Geo node: #{self.url}" self.oauth_application.redirect_uri = oauth_callback_url end def expire_cache! Gitlab::Geo.expire_cache! end # This method is required for backward compatibility. If it # returns true, then we can be fairly confident they did not # set gitlab_rails['geo_node_name']. But if it returns false, # then we aren't sure, so we shouldn't touch the name value. def looks_like_url_field_missing_slash?(value) add_ending_slash(value) == url end def read_with_ending_slash(attribute) value = read_attribute(attribute) add_ending_slash(value) end def write_with_ending_slash(attribute, value) value = add_ending_slash(value) write_attribute(attribute, value) end def add_ending_slash(value) return value if value.blank? return value if value.end_with?('/') "#{value}/" end def projects_for_selected_namespaces Project.where(Project.arel_table.name => { namespace_id: selected_namespaces_and_descendants.select(:id) }) end def projects_for_selected_shards Project.within_shards(selective_sync_shards) end def project_model Project end def uploads_model Upload end end