summaryrefslogtreecommitdiff
path: root/app/services/projects/update_pages_service.rb
blob: dc75fe1014aff86c83441eb6abc2332ee216b22a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# frozen_string_literal: true

module Projects
  class UpdatePagesService < BaseService
    InvalidStateError = Class.new(StandardError)
    BLOCK_SIZE = 32.kilobytes
    PUBLIC_DIR = 'public'

    # old deployment can be cached by pages daemon
    # so we need to give pages daemon some time update cache
    # 10 minutes is enough, but 30 feels safer
    OLD_DEPLOYMENTS_DESTRUCTION_DELAY = 30.minutes.freeze

    attr_reader :build

    def initialize(project, build)
      @project = project
      @build = build
    end

    def execute
      register_attempt

      # Create status notifying the deployment of pages
      @status = build_commit_status
      ::Ci::Pipelines::AddJobService.new(@build.pipeline).execute!(@status) do |job|
        job.enqueue!
        job.run!
      end

      validate_state!
      validate_max_size!
      validate_max_entries!

      build.artifacts_file.use_file do |artifacts_path|
        create_pages_deployment(artifacts_path, build)
        success
      end
    rescue InvalidStateError => e
      error(e.message)
    rescue StandardError => e
      error(e.message)
      raise e
    end

    private

    def success
      @status.success
      @project.mark_pages_as_deployed(artifacts_archive: build.job_artifacts_archive)
      super
    end

    def error(message)
      register_failure
      log_error("Projects::UpdatePagesService: #{message}")
      @status.allow_failure = !latest?
      @status.description = message
      @status.drop(:script_failure)
      super
    end

    def build_commit_status
      GenericCommitStatus.new(
        user: build.user,
        stage: 'deploy',
        name: 'pages:deploy'
      )
    end

    def create_pages_deployment(artifacts_path, build)
      sha256 = build.job_artifacts_archive.file_sha256

      deployment = nil
      File.open(artifacts_path) do |file|
        deployment = project.pages_deployments.create!(file: file,
                                                       file_count: entries_count,
                                                       file_sha256: sha256,
                                                       ci_build_id: build.id
                                                      )

        validate_outdated_sha!

        project.update_pages_deployment!(deployment)
      end

      DestroyPagesDeploymentsWorker.perform_in(
        OLD_DEPLOYMENTS_DESTRUCTION_DELAY,
        project.id,
        deployment.id
      )
    end

    def ref
      build.ref
    end

    def artifacts
      build.artifacts_file.path
    end

    def latest_sha
      project.commit(build.ref).try(:sha).to_s
    ensure
      # Close any file descriptors that were opened and free libgit2 buffers
      project.cleanup
    end

    def sha
      build.sha
    end

    def register_attempt
      pages_deployments_total_counter.increment
    end

    def register_failure
      pages_deployments_failed_total_counter.increment
    end

    def pages_deployments_total_counter
      @pages_deployments_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered")
    end

    def pages_deployments_failed_total_counter
      @pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
    end

    def validate_state!
      raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
      raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?

      validate_outdated_sha!
    end

    def validate_outdated_sha!
      return if latest?

      if Feature.enabled?(:pages_smart_check_outdated_sha, project, default_enabled: :yaml)
        # use pipeline_id in case the build is retried
        last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id

        return unless last_deployed_pipeline_id
        return if last_deployed_pipeline_id <= build.pipeline_id
      end

      raise InvalidStateError, 'build SHA is outdated for this ref'
    end

    def latest?
      # check if sha for the ref is still the most recent one
      # this helps in case when multiple deployments happens
      sha == latest_sha
    end

    def validate_max_size!
      if total_size > max_size
        raise InvalidStateError, "artifacts for pages are too large: #{total_size}"
      end
    end

    # Calculate page size after extract
    def total_size
      @total_size ||= build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true).total_size
    end

    def max_size_from_settings
      Gitlab::CurrentSettings.max_pages_size.megabytes
    end

    def max_size
      max_pages_size = max_size_from_settings

      return ::Gitlab::Pages::MAX_SIZE if max_pages_size == 0

      max_pages_size
    end

    def validate_max_entries!
      if pages_file_entries_limit > 0 && entries_count > pages_file_entries_limit
        raise InvalidStateError, "pages site contains #{entries_count} file entries, while limit is set to #{pages_file_entries_limit}"
      end
    end

    def entries_count
      # we're using the full archive and pages daemon needs to read it
      # so we want the total count from entries, not only "public/" directory
      # because it better approximates work we need to do before we can serve the site
      @entries_count = build.artifacts_metadata_entry("", recursive: true).entries.count
    end

    def pages_file_entries_limit
      project.actual_limits.pages_file_entries
    end
  end
end

Projects::UpdatePagesService.prepend_mod_with('Projects::UpdatePagesService')