namespace.rb 11.2 KB
Newer Older
gfyoung's avatar
gfyoung committed
1
2
# frozen_string_literal: true

3
4
5
6
module EE
  # Namespace EE mixin
  #
  # This module is intended to encapsulate EE-specific model logic
7
  # and be prepended in the `Namespace` model
8
  module Namespace
9
    extend ActiveSupport::Concern
10
    extend ::Gitlab::Utils::Override
Lin Jen-Shin's avatar
Lin Jen-Shin committed
11
    include ::Gitlab::Utils::StrongMemoize
12

13
    NAMESPACE_PLANS_TO_LICENSE_PLANS = {
14
15
16
17
      Plan::BRONZE        => License::STARTER_PLAN,
      Plan::SILVER        => License::PREMIUM_PLAN,
      Plan::GOLD          => License::ULTIMATE_PLAN,
      Plan::EARLY_ADOPTER => License::EARLY_ADOPTER_PLAN
18
19
    }.freeze

20
    LICENSE_PLANS_TO_NAMESPACE_PLANS = NAMESPACE_PLANS_TO_LICENSE_PLANS.invert.freeze
21
    PLANS = (NAMESPACE_PLANS_TO_LICENSE_PLANS.keys + [Plan::FREE]).freeze
22

23
24
    CI_USAGE_ALERT_LEVELS = [30, 5].freeze

25
    prepended do
26
27
      include EachBatch

28
29
      attr_writer :root_ancestor

30
31
      belongs_to :plan

32
      has_one :namespace_statistics
33
      has_one :gitlab_subscription, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
34

35
36
      accepts_nested_attributes_for :gitlab_subscription

37
      scope :with_plan, -> { where.not(plan_id: nil) }
38
39
      scope :with_shared_runners_minutes_limit, -> { where("namespaces.shared_runners_minutes_limit > 0") }
      scope :with_extra_shared_runners_minutes_limit, -> { where("namespaces.extra_shared_runners_minutes_limit > 0") }
40
41
42
43
44
45
46
47
48
      scope :with_shared_runners_minutes_exceeding_default_limit, -> do
        where('namespace_statistics.namespace_id = namespaces.id')
        .where('namespace_statistics.shared_runners_seconds > (namespaces.shared_runners_minutes_limit * 60)')
      end

      scope :with_ci_minutes_notification_sent, -> do
        where('last_ci_minutes_notification_at IS NOT NULL OR last_ci_minutes_usage_notification_level IS NOT NULL')
      end

49
50
51
52
53
54
55
56
      scope :with_feature_available_in_plan, -> (feature) do
        plans = plans_with_feature(feature)
        matcher = Plan.where(name: plans)
          .joins(:hosted_subscriptions)
          .where("gitlab_subscriptions.namespace_id = namespaces.id")
          .select('1')
        where("EXISTS (?)", matcher)
      end
57

58
      delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
59
        :extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true
60

Nick Thomas's avatar
Nick Thomas committed
61
62
63
      # Opportunistically clear the +file_template_project_id+ if invalid
      before_validation :clear_file_template_project_id

64
      validate :validate_plan_name
65
      validate :validate_shared_runner_minutes_support
66

67
      delegate :trial?, :trial_ends_on, :upgradable?, to: :gitlab_subscription, allow_nil: true
68

69
      before_create :sync_membership_lock_with_parent
Nick Thomas's avatar
Nick Thomas committed
70
71
72

      # Changing the plan or other details may invalidate this cache
      before_save :clear_feature_available_cache
73
74
    end

75
    class_methods do
76
77
78
79
80
      def plans_with_feature(feature)
        LICENSE_PLANS_TO_NAMESPACE_PLANS.values_at(*License.plans_with_feature(feature))
      end
    end

81
    override :move_dir
82
83
84
85
86
    def move_dir
      succeeded = super

      if succeeded
        all_projects.each do |project|
87
88
89
          ::Geo::RepositoryRenamedEventStore.new(
            project,
            old_path: project.path,
Michael Kozono's avatar
Michael Kozono committed
90
            old_path_with_namespace: old_path_with_namespace_for(project)
91
          ).create!
92
93
94
95
96
97
        end
      end

      succeeded
    end

Michael Kozono's avatar
Michael Kozono committed
98
    def old_path_with_namespace_for(project)
99
      project.full_path.sub(/\A#{Regexp.escape(full_path)}/, full_path_before_last_save)
Michael Kozono's avatar
Michael Kozono committed
100
101
    end

102
103
104
105
106
107
108
109
110
111
112
113
114
    # This makes the feature disabled by default, in contrary to how
    # `#feature_available?` makes a feature enabled by default.
    #
    # This allows to:
    # - Enable the feature flag for a given group, regardless of the license.
    #   This is useful for early testing a feature in production on a given group.
    # - Enable the feature flag globally and still check that the license allows
    #   it. This is the case when we're ready to enable a feature for anyone
    #   with the correct license.
    def beta_feature_available?(feature)
      ::Feature.enabled?(feature, self) ||
        (::Feature.enabled?(feature) && feature_available?(feature))
    end
115
    alias_method :alpha_feature_available?, :beta_feature_available?
116

117
    # Checks features (i.e. https://about.gitlab.com/pricing/) availabily
118
119
    # for a given Namespace plan. This method should consider ancestor groups
    # being licensed.
120
    override :feature_available?
121
    def feature_available?(feature)
122
123
124
      # This feature might not be behind a feature flag at all, so default to true
      return false unless ::Feature.enabled?(feature, default_enabled: true)

Lin Jen-Shin's avatar
Lin Jen-Shin committed
125
      available_features = strong_memoize(:feature_available) do
126
127
        Hash.new do |h, f|
          h[f] = load_feature_available(f)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
128
        end
Bob Van Landuyt's avatar
Bob Van Landuyt committed
129
      end
130

Lin Jen-Shin's avatar
Lin Jen-Shin committed
131
      available_features[feature]
Bob Van Landuyt's avatar
Bob Van Landuyt committed
132
133
134
    end

    def feature_available_in_plan?(feature)
135
136
      return true if ::License::ANY_PLAN_FEATURES.include?(feature)

Lin Jen-Shin's avatar
Lin Jen-Shin committed
137
      available_features = strong_memoize(:features_available_in_plan) do
138
139
        Hash.new do |h, f|
          h[f] = (plans.map(&:name) & self.class.plans_with_feature(f)).any?
Lin Jen-Shin's avatar
Lin Jen-Shin committed
140
        end
141
      end
Bob Van Landuyt's avatar
Bob Van Landuyt committed
142

Lin Jen-Shin's avatar
Lin Jen-Shin committed
143
      available_features[feature]
144
145
    end

146
    def actual_plan
147
      strong_memoize(:actual_plan) do
148
149
150
151
152
153
        if parent_id
          root_ancestor.actual_plan
        else
          subscription = find_or_create_subscription
          subscription&.hosted_plan || Plan.free || Plan.default
        end
154
      end
155
156
    end

Fabio Pitino's avatar
Fabio Pitino committed
157
158
159
160
161
162
163
    def actual_limits
      # We default to PlanLimits.new otherwise a lot of specs would fail
      # On production each plan should already have associated limits record
      # https://gitlab.com/gitlab-org/gitlab/issues/36037
      actual_plan&.limits || PlanLimits.new
    end

164
    def actual_plan_name
165
      actual_plan&.name || Plan::FREE
166
167
    end

168
169
170
171
172
173
174
175
176
177
    def actual_size_limit
      ::Gitlab::CurrentSettings.repository_size_limit
    end

    def sync_membership_lock_with_parent
      if parent&.membership_lock?
        self.membership_lock = true
      end
    end

178
    def shared_runner_minutes_supported?
179
      !has_parent?
180
181
    end

182
183
184
185
186
187
188
189
    def actual_shared_runners_minutes_limit(include_extra: true)
      extra_minutes = include_extra ? extra_shared_runners_minutes_limit.to_i : 0

      if shared_runners_minutes_limit
        shared_runners_minutes_limit + extra_minutes
      else
        ::Gitlab::CurrentSettings.shared_runners_minutes + extra_minutes
      end
190
191
192
    end

    def shared_runners_minutes_limit_enabled?
193
194
      shared_runner_minutes_supported? &&
        shared_runners_enabled? &&
195
196
197
198
199
200
201
        actual_shared_runners_minutes_limit.nonzero?
    end

    def shared_runners_minutes_used?
      shared_runners_minutes_limit_enabled? &&
        shared_runners_minutes.to_i >= actual_shared_runners_minutes_limit
    end
202

203
204
205
206
207
208
209
210
211
212
213
    def shared_runners_remaining_minutes_percent
      return 0 if shared_runners_remaining_minutes.to_f <= 0
      return 0 if actual_shared_runners_minutes_limit.to_f == 0

      (shared_runners_remaining_minutes.to_f * 100) / actual_shared_runners_minutes_limit.to_f
    end

    def shared_runners_remaining_minutes_below_threshold?
      shared_runners_remaining_minutes_percent.to_i <= last_ci_minutes_usage_notification_level.to_i
    end

214
215
216
217
218
219
    def extra_shared_runners_minutes_used?
      shared_runners_minutes_limit_enabled? &&
        extra_shared_runners_minutes_limit &&
        extra_shared_runners_minutes.to_i >= extra_shared_runners_minutes_limit
    end

ayufanpl's avatar
ayufanpl committed
220
    def shared_runners_enabled?
221
      all_projects.with_shared_runners.any?
ayufanpl's avatar
ayufanpl committed
222
223
    end

224
225
226
    # These helper methods are required to not break the Namespace API.
    def plan=(plan_name)
      if plan_name.is_a?(String)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
227
        @plan_name = plan_name # rubocop:disable Gitlab/ModuleWithInstanceVariables
228

Lin Jen-Shin's avatar
Lin Jen-Shin committed
229
        super(Plan.find_by(name: @plan_name)) # rubocop:disable Gitlab/ModuleWithInstanceVariables
230
231
232
233
234
      else
        super
      end
    end

235
236
237
238
239
240
241
    def memoized_plans=(plans)
      @plans = plans # rubocop: disable Gitlab/ModuleWithInstanceVariables
    end

    def plans
      @plans ||=
        if parent_id
242
          Plan.hosted_plans_for_namespaces(self_and_ancestors.select(:id))
243
        else
244
          Plan.hosted_plans_for_namespaces(self)
245
246
247
        end
    end

248
249
250
251
252
253
254
255
    # When a purchasing a GL.com plan for a User namespace
    # we only charge for a single user.
    # This method is overwritten in Group where we made the calculation
    # for Group namespaces.
    def billable_members_count(_requested_hosted_plan = nil)
      1
    end

256
257
258
259
    def eligible_for_trial?
      ::Gitlab.com? &&
        parent_id.nil? &&
        trial_ends_on.blank? &&
260
        [Plan::EARLY_ADOPTER, Plan::FREE].include?(actual_plan_name)
261
262
263
    end

    def trial_active?
264
      trial? && trial_ends_on.present? && trial_ends_on >= Date.today
265
266
    end

267
268
269
270
    def never_had_trial?
      trial_ends_on.nil?
    end

271
272
273
    def trial_expired?
      trial_ends_on.present? &&
        trial_ends_on < Date.today &&
274
        actual_plan_name == Plan::FREE
275
276
    end

Nick Thomas's avatar
Nick Thomas committed
277
278
279
280
281
282
283
284
285
    # A namespace may not have a file template project
    def checked_file_template_project
      nil
    end

    def checked_file_template_project_id
      checked_file_template_project&.id
    end

286
    def store_security_reports_available?
287
288
289
290
      feature_available?(:sast) ||
      feature_available?(:dependency_scanning) ||
      feature_available?(:container_scanning) ||
      feature_available?(:dast)
291
292
    end

293
    def free_plan?
294
      actual_plan_name == Plan::FREE
295
296
297
    end

    def early_adopter_plan?
298
      actual_plan_name == Plan::EARLY_ADOPTER
299
300
301
    end

    def bronze_plan?
302
      actual_plan_name == Plan::BRONZE
303
304
305
    end

    def silver_plan?
306
      actual_plan_name == Plan::SILVER
307
308
309
    end

    def gold_plan?
310
      actual_plan_name == Plan::GOLD
311
312
    end

313
314
315
316
    def use_elasticsearch?
      ::Gitlab::CurrentSettings.elasticsearch_indexes_namespace?(self)
    end

317
318
    private

319
    def validate_plan_name
Lin Jen-Shin's avatar
Lin Jen-Shin committed
320
      if @plan_name.present? && PLANS.exclude?(@plan_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables
321
322
323
324
        errors.add(:plan, 'is not included in the list')
      end
    end

325
326
327
328
329
330
331
332
    def validate_shared_runner_minutes_support
      return if shared_runner_minutes_supported?

      if shared_runners_minutes_limit_changed?
        errors.add(:shared_runners_minutes_limit, 'is not supported for this namespace')
      end
    end

Nick Thomas's avatar
Nick Thomas committed
333
334
335
336
    def clear_feature_available_cache
      clear_memoization(:feature_available)
    end

Bob Van Landuyt's avatar
Bob Van Landuyt committed
337
338
339
    def load_feature_available(feature)
      globally_available = License.feature_available?(feature)

340
      if ::Gitlab::CurrentSettings.should_check_namespace_plan?
Bob Van Landuyt's avatar
Bob Van Landuyt committed
341
342
343
344
345
        globally_available && feature_available_in_plan?(feature)
      else
        globally_available
      end
    end
Nick Thomas's avatar
Nick Thomas committed
346
347
348
349
350
351
352

    def clear_file_template_project_id
      return unless has_attribute?(:file_template_project_id)
      return if checked_file_template_project_id.present?

      self.file_template_project_id = nil
    end
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368

    def find_or_create_subscription
      # Hosted subscriptions are only available for root groups for now.
      return if parent_id

      gitlab_subscription || generate_subscription
    end

    def generate_subscription
      create_gitlab_subscription(
        plan_code: plan&.name,
        trial: trial_active?,
        start_date: created_at,
        seats: 0
      )
    end
369
370
371
372

    def shared_runners_remaining_minutes
      [actual_shared_runners_minutes_limit.to_f - shared_runners_minutes.to_f, 0].max
    end
373
374
  end
end