update_pages_service.rb 6.0 KB
Newer Older
1
module Projects
K
Kamil Trzcinski 已提交
2
  class UpdatePagesService < BaseService
S
Shinya Maeda 已提交
3
    InvaildStateError = Class.new(StandardError)
4
    FailedToExtractError = Class.new(StandardError)
5

6 7
    BLOCK_SIZE = 32.kilobytes
    MAX_SIZE = 1.terabyte
D
Douwe Maan 已提交
8
    SITE_PATH = 'public/'.freeze
9 10 11 12 13 14 15 16

    attr_reader :build

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

    def execute
S
Shinya Maeda 已提交
17 18
      register_attempt

19 20
      # Create status notifying the deployment of pages
      @status = create_status
K
Kamil Trzcinski 已提交
21
      @status.enqueue!
22 23
      @status.run!

S
Shinya Maeda 已提交
24 25
      raise InvaildStateError, 'missing pages artifacts' unless build.artifacts?
      raise InvaildStateError, 'pages are outdated' unless latest?
26 27 28 29 30 31 32 33

      # Create temporary directory in which we will extract the artifacts
      FileUtils.mkdir_p(tmp_path)
      Dir.mktmpdir(nil, tmp_path) do |archive_path|
        extract_archive!(archive_path)

        # Check if we did extract public directory
        archive_public_path = File.join(archive_path, 'public')
34
        raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
S
Shinya Maeda 已提交
35
        raise InvaildStateError, 'pages are outdated' unless latest?
36 37 38 39

        deploy_page!(archive_public_path)
        success
      end
40
    rescue InvaildStateError, FailedToExtractError => e
Z
Z.J. van de Weg 已提交
41
      register_failure
42 43 44 45 46 47 48
      error(e.message)
    end

    private

    def success
      @status.success
S
Shinya Maeda 已提交
49
      delete_artifact!
50 51 52 53
      super
    end

    def error(message, http_status = nil)
D
Danilo Bargen 已提交
54
      log_error("Projects::UpdatePagesService: #{message}")
55 56
      @status.allow_failure = !latest?
      @status.description = message
57
      @status.drop(:script_failure)
S
Shinya Maeda 已提交
58
      delete_artifact!
59 60 61 62 63 64
      super
    end

    def create_status
      GenericCommitStatus.new(
        project: project,
65
        pipeline: build.pipeline,
66 67 68 69 70 71 72 73
        user: build.user,
        ref: build.ref,
        stage: 'deploy',
        name: 'pages:deploy'
      )
    end

    def extract_archive!(temp_path)
M
Micaël Bergeron 已提交
74
      if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz')
75
        extract_tar_archive!(temp_path)
M
Micaël Bergeron 已提交
76
      elsif artifacts.ends_with?('.zip')
77 78
        extract_zip_archive!(temp_path)
      else
79
        raise FailedToExtractError, 'unsupported artifacts format'
80 81 82 83
      end
    end

    def extract_tar_archive!(temp_path)
84 85 86 87 88
      build.artifacts_file.use_file do |artifacts_path|
        results = Open3.pipeline(%W(gunzip -c #{artifacts_path}),
                                %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
                                %W(tar -x -C #{temp_path} #{SITE_PATH}),
                                err: '/dev/null')
M
Micaël Bergeron 已提交
89
        raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
90
      end
91 92
    end

93
    def extract_zip_archive!(temp_path)
94
      raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata?
95 96 97 98 99

      # Calculate page size after extract
      public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)

      if public_entry.total_size > max_size
100
        raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}"
101 102 103
      end

      # Requires UnZip at least 6.00 Info-ZIP.
104
      # -qq be (very) quiet
105 106 107
      # -n  never overwrite existing files
      # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
      site_path = File.join(SITE_PATH, '*')
108 109
      build.artifacts_file.use_file do |artifacts_path|
        unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
M
Micaël Bergeron 已提交
110
          raise FailedToExtractError, 'pages failed to extract'
111
        end
112 113 114
      end
    end

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
    def deploy_page!(archive_public_path)
      # Do atomic move of pages
      # Move and removal may not be atomic, but they are significantly faster then extracting and removal
      # 1. We move deployed public to previous public path (file removal is slow)
      # 2. We move temporary public to be deployed public
      # 3. We remove previous public path
      FileUtils.mkdir_p(pages_path)
      begin
        FileUtils.move(public_path, previous_public_path)
      rescue
      end
      FileUtils.move(archive_public_path, public_path)
    ensure
      FileUtils.rm_r(previous_public_path, force: true)
    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 blocks
      # Calculate dd parameters: we limit the size of pages
139 140 141 142
      1 + max_size / BLOCK_SIZE
    end

    def max_size
143
      max_pages_size = Gitlab::CurrentSettings.max_pages_size.megabytes
144 145 146 147

      return MAX_SIZE if max_pages_size.zero?

      [max_pages_size, MAX_SIZE].min
148 149 150
    end

    def tmp_path
151
      @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
    end

    def pages_path
      @pages_path ||= project.pages_path
    end

    def public_path
      @public_path ||= File.join(pages_path, 'public')
    end

    def previous_public_path
      @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
    end

    def ref
      build.ref
    end

M
Micaël Bergeron 已提交
170 171 172 173 174 175 176 177 178
    def artifacts
      build.artifacts_file.path
    end

    def delete_artifact!
      build.reload # Reload stable object to prevent erase artifacts with old state
      build.erase_artifacts! unless build.has_expiring_artifacts?
    end

179 180
    def latest_sha
      project.commit(build.ref).try(:sha).to_s
181 182 183
    ensure
      # Close any file descriptors that were opened and free libgit2 buffers
      project.cleanup
184 185 186 187 188
    end

    def sha
      build.sha
    end
Z
Z.J. van de Weg 已提交
189 190

    def register_attempt
191
      pages_deployments_total_counter.increment
Z
Z.J. van de Weg 已提交
192 193 194
    end

    def register_failure
195
      pages_deployments_failed_total_counter.increment
Z
Z.J. van de Weg 已提交
196 197 198 199 200 201 202 203 204
    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
205 206
  end
end