summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_PAGES_VERSION1
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock4
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/projects/pages_controller.rb22
-rw-r--r--app/controllers/projects/pages_domains_controller.rb49
-rw-r--r--app/models/ci/build.rb3
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/pages_domain.rb119
-rw-r--r--app/models/project.rb43
-rw-r--r--app/policies/project_policy.rb4
-rw-r--r--app/services/pages_service.rb15
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/update_pages_configuration_service.rb69
-rw-r--r--app/services/projects/update_pages_service.rb164
-rw-r--r--app/validators/certificate_key_validator.rb25
-rw-r--r--app/validators/certificate_validator.rb24
-rw-r--r--app/views/admin/application_settings/_form.html.haml8
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml4
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/pages/_access.html.haml13
-rw-r--r--app/views/projects/pages/_destroy.haml12
-rw-r--r--app/views/projects/pages/_disabled.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml17
-rw-r--r--app/views/projects/pages/_no_domains.html.haml7
-rw-r--r--app/views/projects/pages/_use.html.haml8
-rw-r--r--app/views/projects/pages/show.html.haml26
-rw-r--r--app/views/projects/pages_domains/_form.html.haml34
-rw-r--r--app/views/projects/pages_domains/new.html.haml6
-rw-r--r--app/views/projects/pages_domains/show.html.haml30
-rw-r--r--app/workers/pages_worker.rb23
-rw-r--r--changelogs/unreleased/jej-pages-picked-from-ee.yml4
-rw-r--r--config/gitlab.yml.example15
-rw-r--r--config/initializers/1_settings.rb38
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20151215132013_add_pages_size_to_application_settings.rb14
-rw-r--r--db/migrate/20160210105555_create_pages_domain.rb16
-rw-r--r--db/schema.rb18
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/high_availability/load_balancer.md2
-rw-r--r--doc/administration/pages/index.md249
-rw-r--r--doc/administration/pages/source.md323
-rw-r--r--doc/ci/yaml/README.md29
-rw-r--r--doc/install/installation.md7
-rw-r--r--doc/pages/README.md1
-rw-r--r--doc/pages/administration.md1
-rw-r--r--doc/raketasks/backup_restore.md23
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/university/support/README.md2
-rw-r--r--doc/user/permissions.md3
-rw-r--r--doc/user/project/pages/img/pages_create_project.pngbin0 -> 33597 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin0 -> 87071 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin0 -> 34686 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin0 -> 63716 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin0 -> 51136 bytes
-rw-r--r--doc/user/project/pages/img/pages_remove.pngbin0 -> 27259 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin0 -> 103730 bytes
-rw-r--r--doc/user/project/pages/index.md434
-rw-r--r--features/project/active_tab.feature7
-rw-r--r--features/project/pages.feature82
-rw-r--r--features/steps/project/active_tab.rb8
-rw-r--r--features/steps/project/pages.rb139
-rw-r--r--features/steps/shared/project.rb6
-rw-r--r--lib/api/settings.rb3
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/backup/pages.rb13
-rw-r--r--lib/gitlab/pages_transfer.rb7
-rw-r--r--lib/gitlab/project_transfer.rb35
-rw-r--r--lib/gitlab/uploads_transfer.rb30
-rwxr-xr-xlib/support/init.d/gitlab68
-rw-r--r--[-rwxr-xr-x]lib/support/init.d/gitlab.default.example24
-rw-r--r--lib/support/nginx/gitlab-pages28
-rw-r--r--lib/support/nginx/gitlab-pages-ssl77
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--shared/pages/.gitkeep0
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb64
-rw-r--r--spec/factories/pages_domains.rb153
-rw-r--r--spec/features/projects/pages_spec.rb60
-rw-r--r--spec/fixtures/pages.tar.gzbin0 -> 1795 bytes
-rw-r--r--spec/fixtures/pages.zipbin0 -> 1851 bytes
-rw-r--r--spec/fixtures/pages.zip.metabin0 -> 225 bytes
-rw-r--r--spec/fixtures/pages_empty.tar.gzbin0 -> 128 bytes
-rw-r--r--spec/fixtures/pages_empty.zipbin0 -> 160 bytes
-rw-r--r--spec/fixtures/pages_empty.zip.metabin0 -> 116 bytes
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/project_transfer_spec.rb (renamed from spec/lib/gitlab/uploads_transfer_spec.rb)11
-rw-r--r--spec/models/pages_domain_spec.rb168
-rw-r--r--spec/models/project_spec.rb44
-rw-r--r--spec/routing/project_routing_spec.rb37
-rw-r--r--spec/services/pages_service_spec.rb47
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_configuration_service_spec.rb24
-rw-r--r--spec/services/projects/update_pages_service_spec.rb80
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb14
95 files changed, 3115 insertions, 71 deletions
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
new file mode 100644
index 00000000000..abd410582de
--- /dev/null
+++ b/GITLAB_PAGES_VERSION
@@ -0,0 +1 @@
+0.2.4
diff --git a/Gemfile b/Gemfile
index 479df411881..51eeec965f2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -48,6 +48,9 @@ gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
+# GitLab Pages
+gem 'validates_hostname', '~> 1.0.6'
+
# Browser detection
gem 'browser', '~> 2.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index f6b889dcca4..7edc12ba348 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -799,6 +799,9 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
+ validates_hostname (1.0.6)
+ activerecord (>= 3.0)
+ activesupport (>= 3.0)
version_sorter (2.1.0)
virtus (1.0.5)
axiom-types (~> 0.1)
@@ -1014,6 +1017,7 @@ DEPENDENCIES
unf (~> 0.1.4)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
+ validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 543d5eac504..df8682e246e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,6 +109,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:plantuml_url,
:max_artifacts_size,
:max_attachment_size,
+ :max_pages_size,
:metrics_enabled,
:metrics_host,
:metrics_method_call_threshold,
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
new file mode 100644
index 00000000000..fbd18b68141
--- /dev/null
+++ b/app/controllers/projects/pages_controller.rb
@@ -0,0 +1,22 @@
+class Projects::PagesController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_read_pages!, only: [:show]
+ before_action :authorize_update_pages!, except: [:show]
+
+ def show
+ @domains = @project.pages_domains.order(:domain)
+ end
+
+ def destroy
+ project.remove_pages
+ project.pages_domains.destroy_all
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Pages were removed')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
new file mode 100644
index 00000000000..b8c253f6ae3
--- /dev/null
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -0,0 +1,49 @@
+class Projects::PagesDomainsController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_update_pages!, except: [:show]
+ before_action :domain, only: [:show, :destroy]
+
+ def show
+ end
+
+ def new
+ @domain = @project.pages_domains.new
+ end
+
+ def create
+ @domain = @project.pages_domains.create(pages_domain_params)
+
+ if @domain.valid?
+ redirect_to namespace_project_pages_path(@project.namespace, @project)
+ else
+ render 'new'
+ end
+ end
+
+ def destroy
+ @domain.destroy
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Domain was removed')
+ end
+ format.js
+ end
+ end
+
+ private
+
+ def pages_domain_params
+ params.require(:pages_domain).permit(
+ :certificate,
+ :key,
+ :domain
+ )
+ end
+
+ def domain
+ @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b1f77bf242c..44d4fb9d8d8 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -256,7 +256,7 @@ module Ci
end
def project_id
- pipeline.project_id
+ gl_project_id
end
def project_name
@@ -451,6 +451,7 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
+ PagesService.new(build_data).execute
project.running_or_pending_build_count(force: true)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 67d8c1c2e4c..2fb2eb44aaa 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -130,6 +130,7 @@ class Namespace < ActiveRecord::Base
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
remove_exports!
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
new file mode 100644
index 00000000000..0b9ebf1ffe2
--- /dev/null
+++ b/app/models/pages_domain.rb
@@ -0,0 +1,119 @@
+class PagesDomain < ActiveRecord::Base
+ belongs_to :project
+
+ validates :domain, hostname: true
+ validates_uniqueness_of :domain, case_sensitive: false
+ validates :certificate, certificate: true, allow_nil: true, allow_blank: true
+ validates :key, certificate_key: true, allow_nil: true, allow_blank: true
+
+ validate :validate_pages_domain
+ validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
+ validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }
+
+ attr_encrypted :key,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ after_create :update
+ after_save :update
+ after_destroy :update
+
+ def to_param
+ domain
+ end
+
+ def url
+ return unless domain
+
+ if certificate
+ "https://#{domain}"
+ else
+ "http://#{domain}"
+ end
+ end
+
+ def has_matching_key?
+ return false unless x509
+ return false unless pkey
+
+ # We compare the public key stored in certificate with public key from certificate key
+ x509.check_private_key(pkey)
+ end
+
+ def has_intermediates?
+ return false unless x509
+
+ # self-signed certificates doesn't have the certificate chain
+ return true if x509.verify(x509.public_key)
+
+ store = OpenSSL::X509::Store.new
+ store.set_default_paths
+
+ # This forces to load all intermediate certificates stored in `certificate`
+ Tempfile.open('certificate_chain') do |f|
+ f.write(certificate)
+ f.flush
+ store.add_file(f.path)
+ end
+
+ store.verify(x509)
+ rescue OpenSSL::X509::StoreError
+ false
+ end
+
+ def expired?
+ return false unless x509
+ current = Time.new
+ current < x509.not_before || x509.not_after < current
+ end
+
+ def subject
+ return unless x509
+ x509.subject.to_s
+ end
+
+ def certificate_text
+ @certificate_text ||= x509.try(:to_text)
+ end
+
+ private
+
+ def update
+ ::Projects::UpdatePagesConfigurationService.new(project).execute
+ end
+
+ def validate_matching_key
+ unless has_matching_key?
+ self.errors.add(:key, "doesn't match the certificate")
+ end
+ end
+
+ def validate_intermediates
+ unless has_intermediates?
+ self.errors.add(:certificate, 'misses intermediates')
+ end
+ end
+
+ def validate_pages_domain
+ return unless domain
+ if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
+ self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
+ end
+ end
+
+ def x509
+ return unless certificate
+ @x509 ||= OpenSSL::X509::Certificate.new(certificate)
+ rescue OpenSSL::X509::CertificateError
+ nil
+ end
+
+ def pkey
+ return unless key
+ @pkey ||= OpenSSL::PKey::RSA.new(key)
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
+ nil
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0d286bfbaa8..7c5fdad5122 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -53,6 +53,8 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
+ after_destroy :remove_pages
+
# update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
@@ -148,6 +150,7 @@ class Project < ActiveRecord::Base
has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :pages_domains, dependent: :destroy
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
@@ -955,6 +958,7 @@ class Project < ActiveRecord::Base
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
+ Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path)
end
# Expires various caches before a project is renamed.
@@ -1156,6 +1160,45 @@ class Project < ActiveRecord::Base
ensure_runners_token!
end
+ def pages_deployed?
+ Dir.exist?(public_pages_path)
+ end
+
+ def pages_url
+ # The hostname always needs to be in downcased
+ # All web servers convert hostname to lowercase
+ host = "#{namespace.path}.#{Settings.pages.host}".downcase
+
+ # The host in URL always needs to be downcased
+ url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
+ "#{prefix}#{namespace.path}."
+ end.downcase
+
+ # If the project path is the same as host, we serve it as group page
+ return url if host == path
+
+ "#{url}/#{path}"
+ end
+
+ def pages_path
+ File.join(Settings.pages.path, path_with_namespace)
+ end
+
+ def public_pages_path
+ File.join(pages_path, 'public')
+ end
+
+ def remove_pages
+ # 1. We rename pages to temporary directory
+ # 2. We wait 5 minutes, due to NFS caching
+ # 3. We asynchronously remove pages with force
+ temp_path = "#{path}.#{SecureRandom.hex}.deleted"
+
+ if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path)
+ PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path)
+ end
+ end
+
def wiki
@wiki ||= ProjectWiki.new(self, self.owner)
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 71ef8901932..f5fd50745aa 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -110,6 +110,9 @@ class ProjectPolicy < BasePolicy
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
+ can! :admin_pages
+ can! :read_pages
+ can! :update_pages
end
def public_access!
@@ -136,6 +139,7 @@ class ProjectPolicy < BasePolicy
can! :remove_fork_project
can! :destroy_merge_request
can! :destroy_issue
+ can! :remove_pages
end
def team_member_owner_access!
diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb
new file mode 100644
index 00000000000..446eeb34d3b
--- /dev/null
+++ b/app/services/pages_service.rb
@@ -0,0 +1,15 @@
+class PagesService
+ attr_reader :data
+
+ def initialize(data)
+ @data = data
+ end
+
+ def execute
+ return unless Settings.pages.enabled
+ return unless data[:build_name] == 'pages'
+ return unless data[:build_status] == 'success'
+
+ PagesWorker.perform_async(:deploy, data[:build_id])
+ end
+end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 34ec575e808..20b049b5973 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -64,6 +64,9 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ # Move pages
+ Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
new file mode 100644
index 00000000000..eb4809afa85
--- /dev/null
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -0,0 +1,69 @@
+module Projects
+ class UpdatePagesConfigurationService < BaseService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ update_file(pages_config_file, pages_config.to_json)
+ reload_daemon
+ success
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def pages_config
+ {
+ domains: pages_domains_config
+ }
+ end
+
+ def pages_domains_config
+ project.pages_domains.map do |domain|
+ {
+ domain: domain.domain,
+ certificate: domain.certificate,
+ key: domain.key,
+ }
+ end
+ end
+
+ def reload_daemon
+ # GitLab Pages daemon constantly watches for modification time of `pages.path`
+ # It reloads configuration when `pages.path` is modified
+ update_file(pages_update_file, SecureRandom.hex(64))
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def pages_config_file
+ File.join(pages_path, 'config.json')
+ end
+
+ def pages_update_file
+ File.join(::Settings.pages.path, '.update')
+ end
+
+ def update_file(file, data)
+ unless data
+ FileUtils.remove(file, force: true)
+ return
+ end
+
+ temp_file = "#{file}.#{SecureRandom.hex(16)}"
+ File.open(temp_file, 'w') do |f|
+ f.write(data)
+ end
+ FileUtils.move(temp_file, file, force: true)
+ ensure
+ # In case if the updating fails
+ FileUtils.remove(temp_file, force: true)
+ end
+ end
+end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
new file mode 100644
index 00000000000..f5f9ee88912
--- /dev/null
+++ b/app/services/projects/update_pages_service.rb
@@ -0,0 +1,164 @@
+module Projects
+ class UpdatePagesService < BaseService
+ BLOCK_SIZE = 32.kilobytes
+ MAX_SIZE = 1.terabyte
+ SITE_PATH = 'public/'
+
+ attr_reader :build
+
+ def initialize(project, build)
+ @project, @build = project, build
+ end
+
+ def execute
+ # Create status notifying the deployment of pages
+ @status = create_status
+ @status.enqueue!
+ @status.run!
+
+ raise 'missing pages artifacts' unless build.artifacts_file?
+ raise 'pages are outdated' unless latest?
+
+ # 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')
+ raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise 'pages are outdated' unless latest?
+
+ deploy_page!(archive_public_path)
+ success
+ end
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def success
+ @status.success
+ super
+ end
+
+ def error(message, http_status = nil)
+ @status.allow_failure = !latest?
+ @status.description = message
+ @status.drop
+ super
+ end
+
+ def create_status
+ GenericCommitStatus.new(
+ project: project,
+ pipeline: build.pipeline,
+ user: build.user,
+ ref: build.ref,
+ stage: 'deploy',
+ name: 'pages:deploy'
+ )
+ end
+
+ def extract_archive!(temp_path)
+ if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz')
+ extract_tar_archive!(temp_path)
+ elsif artifacts.ends_with?('.zip')
+ extract_zip_archive!(temp_path)
+ else
+ raise 'unsupported artifacts format'
+ end
+ end
+
+ def extract_tar_archive!(temp_path)
+ results = Open3.pipeline(%W(gunzip -c #{artifacts}),
+ %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
+ %W(tar -x -C #{temp_path} #{SITE_PATH}),
+ err: '/dev/null')
+ raise 'pages failed to extract' unless results.compact.all?(&:success?)
+ end
+
+ def extract_zip_archive!(temp_path)
+ raise 'missing artifacts metadata' unless build.artifacts_metadata?
+
+ # Calculate page size after extract
+ public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+
+ if public_entry.total_size > max_size
+ raise "artifacts for pages are too large: #{public_entry.total_size}"
+ end
+
+ # Requires UnZip at least 6.00 Info-ZIP.
+ # -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, '*')
+ unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path}))
+ raise 'pages failed to extract'
+ end
+ end
+
+ 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
+ 1 + max_size / BLOCK_SIZE
+ end
+
+ def max_size
+ current_application_settings.max_pages_size.megabytes || MAX_SIZE
+ end
+
+ def tmp_path
+ @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ 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
+
+ def artifacts
+ build.artifacts_file.path
+ end
+
+ def latest_sha
+ project.commit(build.ref).try(:sha).to_s
+ end
+
+ def sha
+ build.sha
+ end
+ end
+end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
new file mode 100644
index 00000000000..098b16017d2
--- /dev/null
+++ b/app/validators/certificate_key_validator.rb
@@ -0,0 +1,25 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate_key: true
+# end
+#
+class CertificateKeyValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_private_key_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM private key")
+ end
+ end
+
+ private
+
+ def valid_private_key_pem?(value)
+ return false unless value
+ pkey = OpenSSL::PKey::RSA.new(value)
+ pkey.private?
+ rescue OpenSSL::PKey::PKeyError
+ false
+ end
+end
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
new file mode 100644
index 00000000000..e3d18097f71
--- /dev/null
+++ b/app/validators/certificate_validator.rb
@@ -0,0 +1,24 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate: true
+# end
+#
+class CertificateValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_certificate_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM certificate")
+ end
+ end
+
+ private
+
+ def valid_certificate_pem?(value)
+ return false unless value
+ OpenSSL::X509::Certificate.new(value).present?
+ rescue OpenSSL::X509::CertificateError
+ false
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index e7701d75a6e..b07118b164e 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -187,6 +187,14 @@
.help-block Markdown enabled
%fieldset
+ %legend Pages
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_pages_size, class: 'form-control'
+ .help-block Zero for unlimited
+
+ %fieldset
%legend Continuous Integration
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index c6df66d2c3c..d6c158b6de3 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -34,3 +34,7 @@
= link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
+ = nav_link(controller: :pages) do
+ = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
+ %span
+ Pages
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 7a2dacdb1e7..9c5c1a6d707 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -133,6 +133,7 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
+
.row.prepend-top-default
%hr
.row.prepend-top-default
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
new file mode 100644
index 00000000000..82e20eeebb3
--- /dev/null
+++ b/app/views/projects/pages/_access.html.haml
@@ -0,0 +1,13 @@
+- if @project.pages_deployed?
+ .panel.panel-default
+ .panel-heading
+ Access pages
+ .panel-body
+ %p
+ %strong
+ Congratulations! Your pages are served under:
+
+ %p= link_to @project.pages_url, @project.pages_url
+
+ - @project.pages_domains.each do |domain|
+ %p= link_to domain.url, domain.url
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
new file mode 100644
index 00000000000..42d9ef5ccba
--- /dev/null
+++ b/app/views/projects/pages/_destroy.haml
@@ -0,0 +1,12 @@
+- if @project.pages_deployed?
+ - if can?(current_user, :remove_pages, @project)
+ .panel.panel-default.panel.panel-danger
+ .panel-heading Remove pages
+ .errors-holder
+ .panel-body
+ %p
+ Removing the pages will prevent from exposing them to outside world.
+ .form-actions
+ = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
+ - else
+ .nothing-here-block Only the project owner can remove pages
diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml
new file mode 100644
index 00000000000..ad51fbc6cab
--- /dev/null
+++ b/app/views/projects/pages/_disabled.html.haml
@@ -0,0 +1,4 @@
+.panel.panel-default
+ .nothing-here-block
+ GitLab Pages are disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
new file mode 100644
index 00000000000..4f2dd1a1398
--- /dev/null
+++ b/app/views/projects/pages/_list.html.haml
@@ -0,0 +1,17 @@
+- if can?(current_user, :update_pages, @project) && @domains.any?
+ .panel.panel-default
+ .panel-heading
+ Domains (#{@domains.count})
+ %ul.well-list
+ - @domains.each do |domain|
+ %li
+ .pull-right
+ = link_to 'Details', namespace_project_pages_domain_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_pages_domain_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+ .clearfix
+ %span= link_to domain.domain, domain.url
+ %p
+ - if domain.subject
+ %span.label.label-gray Certificate: #{domain.subject}
+ - if domain.expired?
+ %span.label.label-danger Expired
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
new file mode 100644
index 00000000000..7cea5f3e70b
--- /dev/null
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -0,0 +1,7 @@
+- if can?(current_user, :update_pages, @project)
+ .panel.panel-default
+ .panel-heading
+ Domains
+ .nothing-here-block
+ Support for domains and certificates is disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
new file mode 100644
index 00000000000..9db46f0b1fc
--- /dev/null
+++ b/app/views/projects/pages/_use.html.haml
@@ -0,0 +1,8 @@
+- unless @project.pages_deployed?
+ .panel.panel-info
+ .panel-heading
+ Configure pages
+ .panel-body
+ %p
+ Learn how to upload your static site and have it served by
+ GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}.
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
new file mode 100644
index 00000000000..b6595269b06
--- /dev/null
+++ b/app/views/projects/pages/show.html.haml
@@ -0,0 +1,26 @@
+- page_title 'Pages'
+%h3.page_title
+ Pages
+
+ - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
+ = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do
+ %i.fa.fa-plus
+ New Domain
+
+%p.light
+ With GitLab Pages you can host your static websites on GitLab.
+ Combined with the power of GitLab CI and the help of GitLab Runner
+ you can deploy static pages for your individual projects, your user or your group.
+
+%hr.clearfix
+
+- if Gitlab.config.pages.enabled
+ = render 'access'
+ = render 'use'
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
+ = render 'list'
+ - else
+ = render 'no_domains'
+ = render 'destroy'
+- else
+ = render 'disabled'
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
new file mode 100644
index 00000000000..ca1b41b140a
--- /dev/null
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -0,0 +1,34 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ - if @domain.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ %p= msg
+
+ .form-group
+ = f.label :domain, class: 'control-label' do
+ Domain
+ .col-sm-10
+ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
+
+ - if Gitlab.config.pages.external_https
+ .form-group
+ = f.label :certificate, class: 'control-label' do
+ Certificate (PEM)
+ .col-sm-10
+ = f.text_area :certificate, rows: 5, class: 'form-control'
+ %span.help-inline Upload a certificate for your domain with all intermediates
+
+ .form-group
+ = f.label :key, class: 'control-label' do
+ Key (PEM)
+ .col-sm-10
+ = f.text_area :key, rows: 5, class: 'form-control'
+ %span.help-inline Upload a private key for your certificate
+ - else
+ .nothing-here-block
+ Support for custom certificates is disabled.
+ Ask your system's administrator to enable it.
+
+ .form-actions
+ = f.submit 'Create New Domain', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
new file mode 100644
index 00000000000..e1477c71d06
--- /dev/null
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -0,0 +1,6 @@
+- page_title 'New Pages Domain'
+%h3.page_title
+ New Pages Domain
+%hr.clearfix
+%div
+ = render 'form'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
new file mode 100644
index 00000000000..52dddb052a7
--- /dev/null
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -0,0 +1,30 @@
+- page_title "#{@domain.domain}", 'Pages Domains'
+
+%h3.page-title
+ Pages Domain
+
+.table-holder
+ %table.table
+ %tr
+ %td
+ Domain
+ %td
+ = link_to @domain.domain, @domain.url
+ %tr
+ %td
+ DNS
+ %td
+ %p
+ To access the domain create a new DNS record:
+ %pre
+ #{@domain.domain} CNAME #{@domain.project.namespace.path}.#{Settings.pages.host}.
+ %tr
+ %td
+ Certificate
+ %td
+ - if @domain.certificate_text
+ %pre
+ = @domain.certificate_text
+ - else
+ .light
+ missing
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
new file mode 100644
index 00000000000..4eeb9666bb0
--- /dev/null
+++ b/app/workers/pages_worker.rb
@@ -0,0 +1,23 @@
+class PagesWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :pages, retry: false
+
+ def perform(action, *arg)
+ send(action, *arg)
+ end
+
+ def deploy(build_id)
+ build = Ci::Build.find_by(id: build_id)
+ result = Projects::UpdatePagesService.new(build.project, build).execute
+ if result[:status] == :success
+ result = Projects::UpdatePagesConfigurationService.new(build.project).execute
+ end
+ result
+ end
+
+ def remove(namespace_path, project_path)
+ full_path = File.join(Settings.pages.path, namespace_path, project_path)
+ FileUtils.rm_r(full_path, force: true)
+ end
+end
diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml
new file mode 100644
index 00000000000..ee4a43a93db
--- /dev/null
+++ b/changelogs/unreleased/jej-pages-picked-from-ee.yml
@@ -0,0 +1,4 @@
+---
+title: Added GitLab Pages to CE
+merge_request: 8463
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 42e5f105d46..1cf24e3f3db 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -153,6 +153,21 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## GitLab Pages
+ pages:
+ enabled: false
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ # The domain under which the pages are served:
+ # http://group.example.com/project
+ # or project path can be a group page: group.example.com
+ host: example.com
+ port: 80 # Set to 443 if you serve the pages with HTTPS
+ https: false # Set to true if you serve the pages with HTTPS
+ # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages
+ # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages
+
## Mattermost
## For enabling Add to Mattermost button
mattermost:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4f33aad8693..71046d7860e 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -6,7 +6,7 @@ class Settings < Settingslogic
class << self
def gitlab_on_standard_port?
- gitlab.port.to_i == (gitlab.https ? 443 : 80)
+ on_standard_port?(gitlab)
end
def host_without_www(url)
@@ -14,7 +14,7 @@ class Settings < Settingslogic
end
def build_gitlab_ci_url
- if gitlab_on_standard_port?
+ if on_standard_port?(gitlab)
custom_port = nil
else
custom_port = ":#{gitlab.port}"
@@ -27,6 +27,10 @@ class Settings < Settingslogic
].join('')
end
+ def build_pages_url
+ base_url(pages).join('')
+ end
+
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
@@ -42,11 +46,11 @@ class Settings < Settingslogic
end
def build_base_gitlab_url
- base_gitlab_url.join('')
+ base_url(gitlab).join('')
end
def build_gitlab_url
- (base_gitlab_url + [gitlab.relative_url_root]).join('')
+ (base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
@@ -74,15 +78,19 @@ class Settings < Settingslogic
private
- def base_gitlab_url
- custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
- [ gitlab.protocol,
+ def base_url(config)
+ custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
+ [ config.protocol,
"://",
- gitlab.host,
+ config.host,
custom_port
]
end
+ def on_standard_port?(config)
+ config.port.to_i == (config.https ? 443 : 80)
+ end
+
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
@@ -255,6 +263,20 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
#
+# Pages
+#
+Settings['pages'] ||= Settingslogic.new({})
+Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
+Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root)
+Settings.pages['https'] = false if Settings.pages['https'].nil?
+Settings.pages['host'] ||= "example.com"
+Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
+Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
+Settings.pages['url'] ||= Settings.send(:build_pages_url)
+Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil?
+Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil?
+
+#
# Git LFS
#
Settings['lfs'] ||= Settingslogic.new({})
diff --git a/config/routes/project.rb b/config/routes/project.rb
index efe2fbc521d..ccee39803ed 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -39,6 +39,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resource :pages, only: [:show, :destroy] do
+ resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
+ end
+
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 022b0e80917..56bf4e6b1de 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -50,3 +50,4 @@
- [reactive_caching, 1]
- [cronjob, 1]
- [default, 1]
+ - [pages, 1]
diff --git a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
new file mode 100644
index 00000000000..f3a663f805b
--- /dev/null
+++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
@@ -0,0 +1,14 @@
+class AddPagesSizeToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :application_settings, :max_pages_size, :integer, default: 100, allow_null: false
+ end
+
+ def down
+ remove_column(:application_settings, :max_pages_size)
+ end
+end
diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb
new file mode 100644
index 00000000000..0e8507c7e9a
--- /dev/null
+++ b/db/migrate/20160210105555_create_pages_domain.rb
@@ -0,0 +1,16 @@
+class CreatePagesDomain < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :pages_domains do |t|
+ t.integer :project_id
+ t.text :certificate
+ t.text :encrypted_key
+ t.string :encrypted_key_iv
+ t.string :encrypted_key_salt
+ t.string :domain
+ end
+
+ add_index :pages_domains, :domain, unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c73c311ccb2..50f723a1a0b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -98,17 +98,18 @@ ActiveRecord::Schema.define(version: 20170130204620) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
- t.boolean "sidekiq_throttling_enabled", default: false
- t.string "sidekiq_throttling_queues"
- t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
t.string "plantuml_url"
t.boolean "plantuml_enabled"
+ t.integer "max_pages_size", default: 100, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -855,6 +856,17 @@ ActiveRecord::Schema.define(version: 20170130204620) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "pages_domains", force: :cascade do |t|
+ t.integer "project_id"
+ t.text "certificate"
+ t.text "encrypted_key"
+ t.string "encrypted_key_iv"
+ t.string "encrypted_key_salt"
+ t.string "domain"
+ end
+
+ add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
+
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token", null: false
diff --git a/doc/README.md b/doc/README.md
index 909740211a6..6e94f1e8e87 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -12,6 +12,7 @@
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages.
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
@@ -53,6 +54,7 @@
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
+- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
- [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 1824829903c..dad8e956c0e 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -66,4 +66,4 @@ Read more on high-availability configuration:
configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer.
-[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html
+[gitlab-pages]: ../pages/index.md
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
new file mode 100644
index 00000000000..c352caf1115
--- /dev/null
+++ b/doc/administration/pages/index.md
@@ -0,0 +1,249 @@
+# GitLab Pages Administration
+
+> **Notes:**
+- [Introduced][ee-80] in GitLab EE 8.3.
+- Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+- GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+- This guide is for Omnibus GitLab installations. If you have installed
+ GitLab from source, follow the [Pages source installation document](source.md).
+
+---
+
+This document describes how to set up the _latest_ GitLab Pages feature. Make
+sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
+version as it may include new features and changes needed to be made in your
+configuration.
+
+If you are looking for ways to upload your static content in GitLab Pages, you
+probably want to read the [user documentation][pages-userguide].
+
+## Overview
+
+GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
+written in Go that can listen on an external IP address and provide support for
+custom domains and custom certificates. It supports dynamic certificates through
+SNI and exposes pages using HTTP2 by default.
+You are encouraged to read its [README][pages-readme] to fully understand how
+it works.
+
+---
+
+In the case of custom domains, the Pages daemon needs to listen on ports `80`
+and/or `443`. For that reason, there is some flexibility in the way which you
+can set it up:
+
+1. Run the pages daemon in the same server as GitLab, listening on a secondary IP
+1. Run the pages daemon in a separate server. In that case, the
+ [Pages path](#change-storage-path) must also be present in the server that
+ the pages daemon is installed, so you will have to share it via network.
+1. Run the pages daemon in the same server as GitLab, listening on the same IP
+ but on different ports. In that case, you will have to proxy the traffic with
+ a loadbalancer. If you choose that route note that you should use TCP load
+ balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
+ pages will not be able to be served with user provided certificates. For
+ HTTP it's OK to use HTTP or TCP load balancing.
+
+In this document, we will proceed assuming the first option.
+
+## Prerequisites
+
+Before proceeding with the Pages configuration, you will need to:
+
+1. Have a separate domain under which the GitLab Pages will be served. In this
+ document we assume that to be `example.io`.
+1. Configure a **wildcard DNS record**.
+1. (Optional) Have a **wildcard certificate** for that domain if you decide to
+ serve Pages under HTTPS.
+1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md)
+ so that your users don't have to bring their own.
+
+### DNS configuration
+
+GitLab Pages expect to run on their own virtual host. In your DNS server/provider
+you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
+host that GitLab runs. For example, an entry would look like this:
+
+```
+*.example.io. 1800 IN A 1.2.3.4
+```
+
+where `example.io` is the domain under which GitLab Pages will be served
+and `1.2.3.4` is the IP address of your GitLab instance.
+
+> **Note:**
+You should not use the GitLab domain to serve user pages. For more information
+see the [security section](#security).
+
+[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
+
+## Configuration
+
+Depending on your needs, you can install GitLab Pages in four different ways.
+
+### Option 1. Custom domains with HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "https://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ pages_nginx['enable'] = false
+ gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
+ gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
+ gitlab_pages['external_http'] = '1.1.1.2:80'
+ gitlab_pages['external_https'] = '1.1.1.2:443'
+ ```
+
+ where `1.1.1.1` is the primary IP address that GitLab is listening to and
+ `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 2. Custom domains without HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` and `http://page.com` | no | yes | no | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "http://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ pages_nginx['enable'] = false
+ gitlab_pages['external_http'] = '1.1.1.2:80'
+ ```
+
+ where `1.1.1.1` is the primary IP address that GitLab is listening to and
+ `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 3. Wildcard HTTPS domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` | yes | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Place the certificate and key inside `/etc/gitlab/ssl`
+1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
+
+ ```ruby
+ pages_external_url 'https://example.io'
+
+ pages_nginx['redirect_http_to_https'] = true
+ pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
+ pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key"
+ ```
+
+ where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key,
+ respectively.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 4. Wildcard HTTP domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` | no | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url 'http://example.io'
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['pages_path'] = "/mnt/storage/pages"
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Set maximum pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+## Changelog
+
+GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features
+where added, like custom CNAME and TLS support, and many more are likely to
+come. Below is a brief changelog. If no changes were introduced or a version is
+missing from the changelog, assume that the documentation is the same as the
+latest previous version.
+
+---
+
+**GitLab 8.17 ([documentation][8-17-docs])**
+
+- GitLab Pages were ported to Community Edition in GitLab 8.17.
+- Documentation was refactored to be more modular and easy to follow.
+
+**GitLab 8.5 ([documentation][8-5-docs])**
+
+- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the
+ recommended way to set up GitLab Pages.
+- The [NGINX configs][] have changed to reflect this change. So make sure to
+ update them.
+- Custom CNAME and TLS certificates support.
+- Documentation was moved to one place.
+
+**GitLab 8.3 ([documentation][8-3-docs])**
+
+- GitLab Pages feature was introduced.
+
+[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md
+[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md
+[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md
+[backup]: ../raketasks/backup_restore.md
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[pages-userguide]: ../../user/project/pages/index.md
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
new file mode 100644
index 00000000000..d4468b99992
--- /dev/null
+++ b/doc/administration/pages/source.md
@@ -0,0 +1,323 @@
+# GitLab Pages administration for source installations
+
+This is the documentation for configuring a GitLab Pages when you have installed
+GitLab from source and not using the Omnibus packages.
+
+You are encouraged to read the [Omnibus documentation](index.md) as it provides
+some invaluable information to the configuration of GitLab Pages. Please proceed
+to read it before going forward with this guide.
+
+We also highly recommend that you use the Omnibus GitLab packages, as we
+optimize them specifically for GitLab, and we will take care of upgrading GitLab
+Pages to the latest supported version.
+
+## Overview
+
+[Read the Omnibus overview section.](index.md#overview)
+
+## Prerequisites
+
+[Read the Omnibus prerequisites section.](index.md#prerequisites)
+
+## Configuration
+
+Depending on your needs, you can install GitLab Pages in four different ways.
+
+### Option 1. Custom domains with HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` and `external_https` to the secondary IP on which the pages
+ daemon will listen for connections:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+
+ external_http: 1.1.1.2:80
+ external_https: 1.1.1.2:443
+ ```
+
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`,
+ `external_http` and `external_https` settings that you set above respectively.
+ The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
+ of the `example.io` domain:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
+ `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ listens to.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 2. Custom domains without HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` and `http://page.com` | no | yes | no | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` to the secondary IP on which the pages daemon will listen
+ for connections:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+
+ external_http: 1.1.1.2:80
+ ```
+
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` and `-listen-http` must match the `host` and `external_http`
+ settings that you set above respectively:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
+ `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ listens to.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 3. Wildcard HTTPS domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` | yes | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+1. In `gitlab.yml`, set the port to `443` and https to `true`:
+
+ ```bash
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 4. Wildcard HTTP domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` | no | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Go to the GitLab installation directory:
+
+ ```bash
+ cd /home/git/gitlab
+ ```
+
+1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and
+ the `host` to the FQDN under which GitLab Pages will be served:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+## NGINX caveats
+
+>**Note:**
+The following information applies only for installations from source.
+
+Be extra careful when setting up the domain name in the NGINX config. You must
+not remove the backslashes.
+
+If your GitLab pages domain is `example.io`, replace:
+
+```bash
+server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+```
+
+with:
+
+```
+server_name ~^.*\.example\.io$;
+```
+
+If you are using a subdomain, make sure to escape all dots (`.`) except from
+the first one with a backslash (\). For example `pages.example.io` would be:
+
+```
+server_name ~^.*\.pages\.example\.io$;
+```
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/home/git/gitlab/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `gitlab.yml` under the `pages` section:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ path: /mnt/storage/pages
+ ```
+
+1. [Restart GitLab][restart]
+
+## Set maximum Pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+[backup]: ../raketasks/backup_restore.md
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[pages-userguide]: ../../user/project/pages/index.md
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 06810898cfe..7b9cce32867 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1319,6 +1319,35 @@ with an API call.
[Read more in the triggers documentation.](../triggers/README.md)
+### pages
+
+`pages` is a special job that is used to upload static content to GitLab that
+can be used to serve your website. It has a special syntax, so the two
+requirements below must be met:
+
+1. Any static content must be placed under a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+The example below simply moves all files from the root of the project to the
+`public/` directory. The `.public` workaround is so `cp` doesn't also copy
+`public/` to itself in an infinite loop:
+
+```
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+Read more on [GitLab Pages user documentation](../../pages/README.md).
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index b2d5d51d37d..2b5f8c6d02d 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -313,6 +313,9 @@ sudo usermod -aG redis git
# Change the permissions of the directory where CI artifacts are stored
sudo chmod -R u+rwX shared/artifacts/
+ # Change the permissions of the directory where GitLab Pages are stored
+ sudo chmod -R ug+rwX shared/pages/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -484,6 +487,10 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat
# or else sudo rm -f /etc/nginx/sites-enabled/default
sudo editor /etc/nginx/sites-available/gitlab
+If you intend to enable GitLab pages, there is a separate Nginx config you need
+to use. Read all about the needed configuration at the
+[GitLab Pages administration guide](../administration/pages/index.md).
+
**Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details.
### Test Configuration
diff --git a/doc/pages/README.md b/doc/pages/README.md
new file mode 100644
index 00000000000..c9715eed598
--- /dev/null
+++ b/doc/pages/README.md
@@ -0,0 +1 @@
+This document was moved to [user/project/pages](../user/project/pages/index.md).
diff --git a/doc/pages/administration.md b/doc/pages/administration.md
new file mode 100644
index 00000000000..4eb3bb32c77
--- /dev/null
+++ b/doc/pages/administration.md
@@ -0,0 +1 @@
+This document was moved to [administration/pages](../administration/pages/index.md).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f6b4db71b44..0fb69d63dbe 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -84,6 +84,29 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Exclude specific directories from the backup
+
+You can choose what should be backed up by adding the environment variable `SKIP`.
+The available options are:
+
+* `db`
+* `uploads` (attachments)
+* `repositories`
+* `builds` (CI build output logs)
+* `artifacts` (CI build artifacts)
+* `lfs` (LFS objects)
+* `pages` (pages content)
+
+Use a comma to specify several options at the same time:
+
+```
+# use this command if you've installed GitLab with the Omnibus package
+sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+
+# if you've installed GitLab from source
+sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
+```
+
## Upload backups to remote (cloud) storage
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
diff --git a/doc/university/README.md b/doc/university/README.md
index 12727e9d56f..379a7b4b40f 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -91,7 +91,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
-1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html)
+1. [GitLab Pages Documentation](https://docs.gitlab.com/ce/user/project/pages/)
#### 2.2. GitLab Issues
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 6e415e4d219..ca538ef6dc3 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -172,7 +172,7 @@ Move on to understanding some of GitLab's more advanced features. You can make u
- Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
- Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html)
-- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html)
+- Create your first [GitLab Page](https://docs.gitlab.com/ce/administration/pages/)
- Get to know the GitLab Codebase by reading through the source code:
- Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 678fc3ffd1f..e87cae092a5 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -62,11 +62,14 @@ The following table depicts the various user permission levels in a project.
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
+| Manage pages | | | | ✓ | ✓ |
+| Manage pages domains and certificates | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Force push to protected branches [^3] | | | | | |
| Remove protected branches [^3] | | | | | |
+| Remove pages | | | | | ✓ |
## Group
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
new file mode 100644
index 00000000000..a936d8e5dbd
--- /dev/null
+++ b/doc/user/project/pages/img/pages_create_project.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png
new file mode 100644
index 00000000000..3f615d3757d
--- /dev/null
+++ b/doc/user/project/pages/img/pages_create_user_page.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png
new file mode 100644
index 00000000000..8d34f3b7f38
--- /dev/null
+++ b/doc/user/project/pages/img/pages_dns_details.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png
new file mode 100644
index 00000000000..2bc7cee07a6
--- /dev/null
+++ b/doc/user/project/pages/img/pages_multiple_domains.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png
new file mode 100644
index 00000000000..c3640133bb2
--- /dev/null
+++ b/doc/user/project/pages/img/pages_new_domain_button.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png
new file mode 100644
index 00000000000..adbfb654877
--- /dev/null
+++ b/doc/user/project/pages/img/pages_remove.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png
new file mode 100644
index 00000000000..06d85ab1971
--- /dev/null
+++ b/doc/user/project/pages/img/pages_upload_cert.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
new file mode 100644
index 00000000000..fe477b1ed0b
--- /dev/null
+++ b/doc/user/project/pages/index.md
@@ -0,0 +1,434 @@
+# GitLab Pages
+
+> **Notes:**
+> - This feature was [introduced][ee-80] in GitLab EE 8.3.
+> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+> - This document is about the user guide. To learn how to enable GitLab Pages
+> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
+
+With GitLab Pages you can host for free your static websites on GitLab.
+Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
+deploy static pages for your individual projects, your user or your group.
+
+Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
+information, if you are using GitLab.com to host your website.
+
+## Getting started with GitLab Pages
+
+> **Note:**
+> In the rest of this document we will assume that the general domain name that
+> is used for GitLab Pages is `example.io`.
+
+In general there are two types of pages one might create:
+
+- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
+- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
+
+In GitLab, usernames and groupnames are unique and we often refer to them
+as namespaces. There can be only one namespace in a GitLab instance. Below you
+can see the connection between the type of GitLab Pages, what the project name
+that is created on GitLab looks like and the website URL it will be ultimately
+be served on.
+
+| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
+| -------------------- | ------------ | ----------- |
+| User pages | `username.example.io` | `http(s)://username.example.io` |
+| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
+| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` |
+| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
+
+> **Warning:**
+> There are some known [limitations](#limitations) regarding namespaces served
+> under the general domain name and HTTPS. Make sure to read that section.
+
+### GitLab Pages requirements
+
+In brief, this is what you need to upload your website in GitLab Pages:
+
+1. Find out the general domain name that is used for GitLab Pages
+ (ask your administrator). This is very important, so you should first make
+ sure you get that right.
+1. Create a project
+1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory
+ of your repository with a specific job named [`pages`][pages]
+1. Set up a GitLab Runner to build your website
+
+> **Note:**
+> If [shared runners](../ci/runners/README.md) are enabled by your GitLab
+> administrator, you should be able to use them instead of bringing your own.
+
+### User or group Pages
+
+For user and group pages, the name of the project should be specific to the
+username or groupname and the general domain name that is used for GitLab Pages.
+Head over your GitLab instance that supports GitLab Pages and create a
+repository named `username.example.io`, where `username` is your username on
+GitLab. If the first part of the project name doesn't match exactly your
+username, it won’t work, so make sure to get it right.
+
+To create a group page, the steps are the same like when creating a website for
+users. Just make sure that you are creating the project within the group's
+namespace.
+
+![Create a user-based pages project](img/pages_create_user_page.png)
+
+---
+
+After you push some static content to your repository and GitLab Runner uploads
+the artifacts to GitLab CI, you will be able to access your website under
+`http(s)://username.example.io`. Keep reading to find out how.
+
+>**Note:**
+If your username/groupname contains a dot, for example `foo.bar`, you will not
+be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
+
+### Project Pages
+
+GitLab Pages for projects can be created by both user and group accounts.
+The steps to create a project page for a user or a group are identical:
+
+1. Create a new project
+1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory
+ of your repository with a specific job named [`pages`][pages].
+1. Set up a GitLab Runner to build your website
+
+A user's project will be served under `http(s)://username.example.io/projectname`
+whereas a group's project under `http(s)://groupname.example.io/projectname`.
+
+### Explore the contents of `.gitlab-ci.yml`
+
+The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
+gives you absolute control over the build process. You can actually watch your
+website being built live by following the CI build traces.
+
+> **Note:**
+> Before reading this section, make sure you familiarize yourself with GitLab CI
+> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by
+> following our [quick start guide](../ci/quick_start/README.md).
+
+To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
+rules below:
+
+1. A special job named [`pages`][pages] must be defined
+1. Any static content which will be served by GitLab Pages must be placed under
+ a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+In its simplest form, `.gitlab-ci.yml` looks like:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+```
+
+When the Runner reaches to build the `pages` job, it executes whatever is
+defined in the `script` parameter and if the build completes with a non-zero
+exit status, it then uploads the `public/` directory to GitLab Pages.
+
+The `public/` directory should contain all the static content of your website.
+Depending on how you plan to publish your website, the steps defined in the
+[`script` parameter](../ci/yaml/README.md#script) may differ.
+
+Be aware that Pages are by default branch/tag agnostic and their deployment
+relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
+`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to whatever branch or tag, the Pages will be
+overwritten. In the example below, we limit the Pages to be deployed whenever
+a commit is pushed only on the `master` branch:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab. And since all these parameters were all under a `pages`
+job, the contents of the `public` directory will be served by GitLab Pages.
+
+#### How `.gitlab-ci.yml` looks like when the static content is in your repository
+
+Supposedly your repository contained the following files:
+
+```
+├── index.html
+├── css
+│   └── main.css
+└── js
+ └── main.js
+```
+
+Then the `.gitlab-ci.yml` example below simply moves all files from the root
+directory of the project to the `public/` directory. The `.public` workaround
+is so `cp` doesn't also copy `public/` to itself in an infinite loop:
+
+```yaml
+pages:
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+#### How `.gitlab-ci.yml` looks like when using a static generator
+
+In general, GitLab Pages support any kind of [static site generator][staticgen],
+since `.gitlab-ci.yml` can be configured to run any possible command.
+
+In the root directory of your Git repository, place the source files of your
+favorite static generator. Then provide a `.gitlab-ci.yml` file which is
+specific to your static generator.
+
+The example below, uses [Jekyll] to build the static site:
+
+```yaml
+image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1
+
+pages: # the build job must be named pages
+ script:
+ - gem install jekyll # we install jekyll
+ - jekyll build -d public/ # we tell jekyll to build the site for us
+ artifacts:
+ paths:
+ - public # this is where the site will live and the Runner uploads it in GitLab
+ only:
+ - master # this script is only affecting the master branch
+```
+
+Here, we used the Docker executor and in the first line we specified the base
+image against which our builds will run.
+
+You have to make sure that the generated static files are ultimately placed
+under the `public` directory, that's why in the `script` section we run the
+`jekyll` command that builds the website and puts all content in the `public/`
+directory. Depending on the static generator of your choice, this command will
+differ. Search in the documentation of the static generator you will use if
+there is an option to explicitly set the output directory. If there is not
+such an option, you can always add one more line under `script` to rename the
+resulting directory in `public/`.
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab.
+
+---
+
+See the [jekyll example project][pages-jekyll] to better understand how this
+works.
+
+For a list of Pages projects, see the [example projects](#example-projects) to
+get you started.
+
+#### How to set up GitLab Pages in a repository where there's also actual code
+
+Remember that GitLab Pages are by default branch/tag agnostic and their
+deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
+the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to a branch that will be used specifically for
+your pages.
+
+That way, you can have your project's code in the `master` branch and use an
+orphan branch (let's name it `pages`) that will host your static generator site.
+
+You can create a new empty branch like this:
+
+```bash
+git checkout --orphan pages
+```
+
+The first commit made on this new branch will have no parents and it will be
+the root of a new history totally disconnected from all the other branches and
+commits. Push the source files of your static generator in the `pages` branch.
+
+Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
+one, specifying to execute everything in the `pages` branch:
+
+```
+image: ruby:2.1
+
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - pages
+```
+
+See an example that has different files in the [`master` branch][jekyll-master]
+and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
+also includes `.gitlab-ci.yml`.
+
+[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
+[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
+
+## Next steps
+
+So you have successfully deployed your website, congratulations! Let's check
+what more you can do with GitLab Pages.
+
+### Example projects
+
+Below is a list of example projects for GitLab Pages with a plain HTML website
+or various static site generators. Contributions are very welcome.
+
+- [Plain HTML](https://gitlab.com/pages/plain-html)
+- [Jekyll](https://gitlab.com/pages/jekyll)
+- [Hugo](https://gitlab.com/pages/hugo)
+- [Middleman](https://gitlab.com/pages/middleman)
+- [Hexo](https://gitlab.com/pages/hexo)
+- [Brunch](https://gitlab.com/pages/brunch)
+- [Metalsmith](https://gitlab.com/pages/metalsmith)
+- [Harp](https://gitlab.com/pages/harp)
+
+Visit the GitLab Pages group for a full list of example projects:
+<https://gitlab.com/groups/pages>.
+
+### Add a custom domain to your Pages website
+
+If this setting is enabled by your GitLab administrator, you should be able to
+see the **New Domain** button when visiting your project's settings through the
+gear icon in the top right and then navigating to **Pages**.
+
+![New domain button](img/pages_new_domain_button.png)
+
+---
+
+You can add multiple domains pointing to your website hosted under GitLab.
+Once the domain is added, you can see it listed under the **Domains** section.
+
+![Pages multiple domains](img/pages_multiple_domains.png)
+
+---
+
+As a last step, you need to configure your DNS and add a CNAME pointing to your
+user/group page. Click on the **Details** button of a domain for further
+instructions.
+
+![Pages DNS details](img/pages_dns_details.png)
+
+---
+
+>**Note:**
+Currently there is support only for custom domains on per-project basis. That
+means that if you add a custom domain (`example.com`) for your user website
+(`username.example.io`), a project that is served under `username.example.io/foo`,
+will not be accessible under `example.com/foo`.
+
+### Secure your custom domain website with TLS
+
+When you add a new custom domain, you also have the chance to add a TLS
+certificate. If this setting is enabled by your GitLab administrator, you
+should be able to see the option to upload the public certificate and the
+private key when adding a new domain.
+
+![Pages upload cert](img/pages_upload_cert.png)
+
+### Custom error codes pages
+
+You can provide your own 403 and 404 error pages by creating the `403.html` and
+`404.html` files respectively in the root directory of the `public/` directory
+that will be included in the artifacts. Usually this is the root directory of
+your project, but that may differ depending on your static generator
+configuration.
+
+If the case of `404.html`, there are different scenarios. For example:
+
+- If you use project Pages (served under `/projectname/`) and try to access
+ `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
+ `/projectname/404.html`, and then `/404.html`.
+- If you use user/group Pages (served under `/`) and try to access
+ `/non/existing_file` GitLab Pages will try to serve `/404.html`.
+- If you use a custom domain and try to access `/non/existing_file`, GitLab
+ Pages will try to serve only `/404.html`.
+
+### Remove the contents of your pages
+
+If you ever feel the need to purge your Pages content, you can do so by going
+to your project's settings through the gear icon in the top right, and then
+navigating to **Pages**. Hit the **Remove pages** button and your Pages website
+will be deleted. Simple as that.
+
+![Remove pages](img/pages_remove.png)
+
+## GitLab Pages on GitLab.com
+
+If you are using GitLab.com to host your website, then:
+
+- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
+- Custom domains and TLS support are enabled.
+- Shared runners are enabled by default, provided for free and can be used to
+ build your website. If you want you can still bring your own Runner.
+
+The rest of the guide still applies.
+
+## Limitations
+
+When using Pages under the general domain of a GitLab instance (`*.example.io`),
+you _cannot_ use HTTPS with sub-subdomains. That means that if your
+username/groupname contains a dot, for example `foo.bar`, the domain
+`https://foo.bar.example.io` will _not_ work. This is a limitation of the
+[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+don't redirect HTTP to HTTPS.
+
+[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
+
+## Redirects in GitLab Pages
+
+Since you cannot use any custom server configuration files, like `.htaccess` or
+any `.conf` file for that matter, if you want to redirect a web page to another
+location, you can use the [HTTP meta refresh tag][metarefresh].
+
+Some static site generators provide plugins for that functionality so that you
+don't have to create and edit HTML files manually. For example, Jekyll has the
+[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
+
+## Frequently Asked Questions
+
+### Can I download my generated pages?
+
+Sure. All you need to do is download the artifacts archive from the build page.
+
+### Can I use GitLab Pages if my project is private?
+
+Yes. GitLab Pages don't care whether you set your project's visibility level
+to private, internal or public.
+
+### Do I need to create a user/group website before creating a project website?
+
+No, you don't. You can create your project first and it will be accessed under
+`http(s)://namespace.example.io/projectname`.
+
+## Known issues
+
+For a list of known issues, visit GitLab's [public issue tracker].
+
+---
+
+[jekyll]: http://jekyllrb.com/
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[gitlab ci]: https://about.gitlab.com/gitlab-ci
+[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[pages]: ../ci/yaml/README.md#pages
+[staticgen]: https://www.staticgen.com/
+[pages-jekyll]: https://gitlab.com/pages/jekyll
+[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
+[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index d033e6b167b..5c14c5db665 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -53,6 +53,13 @@ Feature: Project Active Tab
And no other sub navs should be active
And the active main tab should be Settings
+ Scenario: On Project Settings/Pages
+ Given I visit my project's settings page
+ And I click the "Pages" tab
+ Then the active sub nav should be Pages
+ And no other sub navs should be active
+ And the active main tab should be Settings
+
Scenario: On Project Members
Given I visit my project's members page
Then the active sub nav should be Members
diff --git a/features/project/pages.feature b/features/project/pages.feature
new file mode 100644
index 00000000000..87d88348d09
--- /dev/null
+++ b/features/project/pages.feature
@@ -0,0 +1,82 @@
+Feature: Project Pages
+ Background:
+ Given I sign in as a user
+ And I own a project
+
+ Scenario: Pages are disabled
+ Given pages are disabled
+ When I visit the Project Pages
+ Then I should see that GitLab Pages are disabled
+
+ Scenario: I can see the pages usage if not deployed
+ Given pages are enabled
+ When I visit the Project Pages
+ Then I should see the usage of GitLab Pages
+
+ Scenario: I can access the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ Then I should be able to access the Pages
+
+ Scenario: I should message that domains support is disabled
+ Given pages are enabled
+ And pages are deployed
+ And support for external domains is disabled
+ When I visit the Project Pages
+ Then I should see that support for domains is disabled
+
+ Scenario: I should see a new domain button
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit the Project Pages
+ And I should be able to add a New Domain
+
+ Scenario: I should be able to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be able to add a new domain for project in group namespace
+ Given I own a project in some group namespace
+ And pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be denied to add the same domain twice
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see error message that domain already exists
+
+ Scenario: I should message that certificates support is disabled when trying to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ Then I should see that support for certificates is disabled
+
+ Scenario: I should be able to add a new domain with certificate
+ Given pages are enabled
+ And pages are exposed on external HTTPS address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I fill the certificate and key
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I can remove the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ And I click Remove Pages
+ Then The Pages should get removed
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 9f701840f1d..e842d7bec2b 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -35,6 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
click_link('Deploy Keys')
end
+ step 'I click the "Pages" tab' do
+ click_link('Pages')
+ end
+
step 'the active sub nav should be Members' do
ensure_active_sub_nav('Members')
end
@@ -47,6 +51,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
ensure_active_sub_nav('Deploy Keys')
end
+ step 'the active sub nav should be Pages' do
+ ensure_active_sub_nav('Pages')
+ end
+
# Sub Tabs: Commits
step 'I click the "Compare" tab' do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
new file mode 100644
index 00000000000..c80c6273807
--- /dev/null
+++ b/features/steps/project/pages.rb
@@ -0,0 +1,139 @@
+class Spinach::Features::ProjectPages < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+
+ step 'pages are enabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:host).and_return('example.com')
+ allow(Gitlab.config.pages).to receive(:port).and_return(80)
+ allow(Gitlab.config.pages).to receive(:https).and_return(false)
+ end
+
+ step 'pages are disabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ step 'I visit the Project Pages' do
+ visit namespace_project_pages_path(@project.namespace, @project)
+ end
+
+ step 'I should see that GitLab Pages are disabled' do
+ expect(page).to have_content('GitLab Pages are disabled')
+ end
+
+ step 'I should see the usage of GitLab Pages' do
+ expect(page).to have_content('Configure pages')
+ end
+
+ step 'pages are deployed' do
+ pipeline = @project.ensure_pipeline('HEAD', @project.commit('HEAD').sha)
+ build = build(:ci_build,
+ project: @project,
+ pipeline: pipeline,
+ ref: 'HEAD',
+ artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'),
+ artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
+ )
+ result = ::Projects::UpdatePagesService.new(@project, build).execute
+ expect(result[:status]).to eq(:success)
+ end
+
+ step 'I should be able to access the Pages' do
+ expect(page).to have_content('Access pages')
+ end
+
+ step 'I should see that support for domains is disabled' do
+ expect(page).to have_content('Support for domains and certificates is disabled')
+ end
+
+ step 'support for external domains is disabled' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(nil)
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ end
+
+ step 'pages are exposed on external HTTP address' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ end
+
+ step 'pages are exposed on external HTTPS address' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443')
+ end
+
+ step 'I should be able to add a New Domain' do
+ expect(page).to have_content('New Domain')
+ end
+
+ step 'I visit add a new Pages Domain' do
+ visit new_namespace_project_pages_domain_path(@project.namespace, @project)
+ end
+
+ step 'I fill the domain' do
+ fill_in 'Domain', with: 'my.test.domain.com'
+ end
+
+ step 'I click on "Create New Domain"' do
+ click_button 'Create New Domain'
+ end
+
+ step 'I should see a new domain added' do
+ expect(page).to have_content('Domains (1)')
+ expect(page).to have_content('my.test.domain.com')
+ end
+
+ step 'pages domain is added' do
+ @project.pages_domains.create!(domain: 'my.test.domain.com')
+ end
+
+ step 'I should see error message that domain already exists' do
+ expect(page).to have_content('Domain has already been taken')
+ end
+
+ step 'I should see that support for certificates is disabled' do
+ expect(page).to have_content('Support for custom certificates is disabled')
+ end
+
+ step 'I fill the certificate and key' do
+ fill_in 'Certificate (PEM)', with: '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+
+ fill_in 'Key (PEM)', with: '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ step 'I click Remove Pages' do
+ click_link 'Remove pages'
+ end
+
+ step 'The Pages should get removed' do
+ expect(@project.pages_deployed?).to be_falsey
+ end
+end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 7a6707a7dfb..dae248b8b7e 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -7,6 +7,12 @@ module SharedProject
@project.team << [@user, :master]
end
+ step "I own a project in some group namespace" do
+ @group = create(:group, name: 'some group')
+ @project = create(:project, namespace: @group)
+ @project.team << [@user, :master]
+ end
+
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
@project = create(:project, :repository, namespace: @group, public_builds: false)
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c5eff16a5de..5206ee4f521 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -57,6 +57,7 @@ module API
requires :shared_runners_text, type: String, desc: 'Shared runners text '
end
optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
given metrics_enabled: ->(val) { val } do
@@ -115,7 +116,7 @@ module API
:send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
:after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
:home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay,
+ :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
:metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index cefbfdce3bb..f099c0651ac 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,6 +1,6 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry]
FOLDERS_TO_BACKUP = %w[repositories db]
FILE_NAME_SUFFIX = '_gitlab_backup.tar'
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
new file mode 100644
index 00000000000..215ded93bfe
--- /dev/null
+++ b/lib/backup/pages.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Pages < Files
+ def initialize
+ super('pages', Gitlab.config.pages.path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
new file mode 100644
index 00000000000..fb215f27cbd
--- /dev/null
+++ b/lib/gitlab/pages_transfer.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PagesTransfer < ProjectTransfer
+ def root_dir
+ Gitlab.config.pages.path
+ end
+ end
+end
diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb
new file mode 100644
index 00000000000..1bba0b78e2f
--- /dev/null
+++ b/lib/gitlab/project_transfer.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ class ProjectTransfer
+ def move_project(project_path, namespace_path_was, namespace_path)
+ new_namespace_folder = File.join(root_dir, namespace_path)
+ FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
+ from = File.join(root_dir, namespace_path_was, project_path)
+ to = File.join(root_dir, namespace_path, project_path)
+ move(from, to, "")
+ end
+
+ def rename_project(path_was, path, namespace_path)
+ base_dir = File.join(root_dir, namespace_path)
+ move(path_was, path, base_dir)
+ end
+
+ def rename_namespace(path_was, path)
+ move(path_was, path)
+ end
+
+ def root_dir
+ raise NotImplementedError
+ end
+
+ private
+
+ def move(path_was, path, base_dir = nil)
+ base_dir = root_dir unless base_dir
+ from = File.join(base_dir, path_was)
+ to = File.join(base_dir, path)
+ FileUtils.mv(from, to)
+ rescue Errno::ENOENT
+ false
+ end
+ end
+end
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index be8fcc7b2d2..81701831a6a 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,33 +1,5 @@
module Gitlab
- class UploadsTransfer
- def move_project(project_path, namespace_path_was, namespace_path)
- new_namespace_folder = File.join(root_dir, namespace_path)
- FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
- from = File.join(root_dir, namespace_path_was, project_path)
- to = File.join(root_dir, namespace_path, project_path)
- move(from, to, "")
- end
-
- def rename_project(path_was, path, namespace_path)
- base_dir = File.join(root_dir, namespace_path)
- move(path_was, path, base_dir)
- end
-
- def rename_namespace(path_was, path)
- move(path_was, path)
- end
-
- private
-
- def move(path_was, path, base_dir = nil)
- base_dir = root_dir unless base_dir
- from = File.join(base_dir, path_was)
- to = File.join(base_dir, path)
- FileUtils.mv(from, to)
- rescue Errno::ENOENT
- false
- end
-
+ class UploadsTransfer < ProjectTransfer
def root_dir
File.join(Rails.root, "public", "uploads")
end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 31b00ff128a..97414ead3dd 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -42,6 +42,11 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+gitlab_pages_enabled=false
+gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd)
+gitlab_pages_pid_path="$pid_path/gitlab-pages.pid"
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
shell_path="/bin/bash"
# Read configuration variable file if it is present
@@ -89,13 +94,20 @@ check_pids(){
mpid=0
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ -f "$gitlab_pages_pid_path" ]; then
+ gppid=$(cat "$gitlab_pages_pid_path")
+ else
+ gppid=0
+ fi
+ fi
}
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ] || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -144,7 +156,15 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ $gppid -ne 0 ]; then
+ kill -0 "$gppid" 2>/dev/null
+ gitlab_pages_status="$?"
+ else
+ gitlab_pages_status="-1"
+ fi
+ fi
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -186,12 +206,19 @@ check_stale_pids(){
exit 1
fi
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran."
+ if ! rm "$gitlab_pages_pid_path"; then
+ echo "Unable to remove stale pid, exiting"
+ exit 1
+ fi
+ fi
}
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -213,6 +240,9 @@ start_gitlab() {
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Starting GitLab Pages"
+ fi
# Then check if the service is running. If it is: don't start again.
if [ "$web_status" = "0" ]; then
@@ -252,6 +282,16 @@ start_gitlab() {
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages is already running with pid $spid, not restarting"
+ else
+ $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \
+ $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \
+ >> $gitlab_pages_log 2>&1 &
+ fi
+ fi
+
# Wait for the pids to be planted
wait_for_pids
# Finally check the status to tell wether or not GitLab is running
@@ -278,13 +318,17 @@ stop_gitlab() {
echo "Shutting down GitLab MailRoom"
RAILS_ENV=$RAILS_ENV bin/mail_room stop
fi
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "Shutting down gitlab-pages"
+ kill -- $(cat $gitlab_pages_pid_path)
+ fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -298,6 +342,7 @@ stop_gitlab() {
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
+ rm -f "$gitlab_pages_pid_path"
print_status
}
@@ -305,7 +350,7 @@ stop_gitlab() {
## Prints the status of GitLab and its components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -331,7 +376,14 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages with pid $mpid is running."
+ else
+ printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
+ fi
+ fi
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
@@ -362,7 +414,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index cc8617b72ca..e5797d8fe3c 100755..100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+# The GitLab Pages Daemon needs either a separate IP address on which it will
+# listen or use different ports than 80 or 443 that will be forwarded to GitLab
+# Pages Daemon.
+#
+# To enable HTTP support for custom domains add the `-listen-http` directive
+# in `gitlab_pages_options` below.
+# The value of -listen-http must be set to `gitlab.yml > pages > external_http`
+# as well. For example:
+#
+# -listen-http 1.1.1.1:80
+#
+# To enable HTTPS support for custom domains add the `-listen-https`,
+# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
+# The value of -listen-https must be set to `gitlab.yml > pages > external_https`
+# as well. For example:
+#
+# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+#
+# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
+# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
+gitlab_pages_enabled=false
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
+
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
# The default is "false"
diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages
new file mode 100644
index 00000000000..d9746c5c1aa
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages
@@ -0,0 +1,28 @@
+## GitLab
+##
+
+## Pages serving host
+server {
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl
new file mode 100644
index 00000000000..a1ccf266835
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages-ssl
@@ -0,0 +1,77 @@
+## GitLab
+##
+
+## Redirects all HTTP traffic to the HTTPS host
+server {
+ ## Either remove "default_server" from the listen line below,
+ ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
+ ## to be served if you visit any address that your server responds to, eg.
+ ## the ip address of the server (http://x.x.x.x/)
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ return 301 https://$http_host$request_uri;
+
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_access.log;
+}
+
+## Pages serving host
+server {
+ listen 0.0.0.0:443 ssl;
+ listen [::]:443 ipv6only=on ssl http2;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ ## Strong SSL Security
+ ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
+ ssl on;
+ ssl_certificate /etc/nginx/ssl/gitlab-pages.crt;
+ ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key;
+
+ # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs
+ ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 5m;
+
+ ## See app/controllers/application_controller.rb for headers set
+
+ ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL.
+ ## Replace with your ssl_trusted_certificate. For more info see:
+ ## - https://medium.com/devops-programming/4445f4862461
+ ## - https://www.ruby-forum.com/topic/4419319
+ ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
+ # ssl_stapling on;
+ # ssl_stapling_verify on;
+ # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt;
+
+ ## [Optional] Generate a stronger DHE parameter:
+ ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
+ ##
+ # ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index a9f1255e8cf..ffab6f492fb 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:pages:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
@@ -56,6 +57,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?("pages")
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
@@ -159,6 +161,25 @@ namespace :gitlab do
end
end
+ namespace :pages do
+ task create: :environment do
+ $progress.puts "Dumping pages ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("pages")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Pages.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring pages ... ".blue
+ Backup::Pages.new.restore
+ $progress.puts "done".green
+ end
+ end
+
namespace :lfs do
task create: :environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
diff --git a/shared/pages/.gitkeep b/shared/pages/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/pages/.gitkeep
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
new file mode 100644
index 00000000000..2362df895a8
--- /dev/null
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Projects::PagesDomainsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'GET show' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "displays the 'show' page" do
+ get(:show, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe 'GET new' do
+ it "displays the 'new' page" do
+ get(:new, request_params)
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('new')
+ end
+ end
+
+ describe 'POST create' do
+ let(:pages_domain_params) do
+ build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain)
+ end
+
+ it "creates a new pages domain" do
+ expect do
+ post(:create, request_params.merge(pages_domain: pages_domain_params))
+ end.to change { PagesDomain.count }.by(1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "deletes the pages domain" do
+ expect do
+ delete(:destroy, request_params.merge(id: pages_domain.domain))
+ end.to change { PagesDomain.count }.by(-1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
new file mode 100644
index 00000000000..6d2e45f41ba
--- /dev/null
+++ b/spec/factories/pages_domains.rb
@@ -0,0 +1,153 @@
+FactoryGirl.define do
+ factory :pages_domain, class: 'PagesDomain' do
+ domain 'my.domain.com'
+
+ trait :with_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_key do
+ key '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ trait :with_missing_chain do
+ # This certificate is signed with different key
+ # And misses the CA to build trust chain
+ certificate '-----BEGIN CERTIFICATE-----
+MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0
+IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS
+dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAw8RWetIUT0YymSuKvBpClzDv/jQdX0Ch+2iF7f4Lm3lcmoUuXgyhl/WRe5K9
+ONuMHPQlZbeavEbvWb0BsU7geInhsjd/zAu3EP17jfSIXToUdSD20wcSG/yclLdZ
+qhb6NCtHTJKFUI8BktoS7kafkdvmeem/UJFzlvcA6VMyGDkS8ZN39a45R1jGmPEl
+Yk0g1jW7lSKcBLjU1O/Csv59LyWXqBP6jR1vB8ijlUf1IyK8gOk7NHF13GHl7Z3A
+/8zwuEt/pB3yK92o71P+FnSEcJ23zcAalz6H9ajVTzRr/AXttineBNVYnEuPXW+V
+Rsboe+bBO/e4pVKXnQ1F3aMT7QIDAQABo28wbTAMBgNVHRMBAf8EAjAAMB0GA1Ud
+DgQWBBSFwo3rhc26lD8ZVaBVcUY1NyCOLDALBgNVHQ8EBAMCBeAwEQYJYIZIAYb4
+QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI
+hvcNAQEFBQADggEBABppUhunuT7qArM9gZ2gLgcOK8qyZWU8AJulvloaCZDvqGVs
+Qom0iEMBrrt5+8bBevNiB49Tz7ok8NFgLzrlEnOw6y6QGjiI/g8sRKEiXl+ZNX8h
+s8VN6arqT348OU8h2BixaXDmBF/IqZVApGhR8+B4fkCt0VQmdzVuHGbOQXMWJCpl
+WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF
+m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+
+VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_trusted_chain do
+ # This contains
+ # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA'
+ # [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority'
+ certificate '-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
+MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
+bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
+bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
+Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
+ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
+UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
+c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
+MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
+30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
+BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
+bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
+AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
+T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
+ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
+mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
+e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
+P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
+dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
+2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
+V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
+HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
+j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
+0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
+lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
++AZxAeKCINT+b72x
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv
+MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
+ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
+eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow
+gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD
+VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq
+hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw
+AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6
+2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr
+ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt
+4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq
+m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/
+vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT
+8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE
+IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO
+KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO
+GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/
+s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
+JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD
+AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9
+MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy
+bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6
+Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ
+zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj
+Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY
+Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5
+B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx
+PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR
+pu/xO28QOG8=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_expired_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp
+cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow
+HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2ge
+NR1qlNFaSvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLyS
+NT438kdTnY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEA
+ATANBgkqhkiG9w0BAQUFAAOBgQBNj+vWvneyW1KkbVK+b/cVmnYPSfbkHrYK6m8X
+Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+
+Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9
+ZDXgrA==
+-----END CERTIFICATE-----'
+ end
+ end
+end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
new file mode 100644
index 00000000000..11793c0f303
--- /dev/null
+++ b/spec/features/projects/pages_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+feature 'Pages', feature: true do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :master }
+
+ background do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+
+ project.team << [user, role]
+
+ login_as(user)
+ end
+
+ shared_examples 'no pages deployed' do
+ scenario 'does not see anything to destroy' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).not_to have_link('Remove pages')
+ expect(page).not_to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ context 'when user is the owner' do
+ background do
+ project.namespace.update(owner: user)
+ end
+
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Remove pages" link' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_link('Remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+
+ context 'when the user is not the owner' do
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Only the project owner can remove pages" text' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+end
diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz
new file mode 100644
index 00000000000..d0e89378b3e
--- /dev/null
+++ b/spec/fixtures/pages.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip
new file mode 100644
index 00000000000..9558fcd4b94
--- /dev/null
+++ b/spec/fixtures/pages.zip
Binary files differ
diff --git a/spec/fixtures/pages.zip.meta b/spec/fixtures/pages.zip.meta
new file mode 100644
index 00000000000..1e6198a15f0
--- /dev/null
+++ b/spec/fixtures/pages.zip.meta
Binary files differ
diff --git a/spec/fixtures/pages_empty.tar.gz b/spec/fixtures/pages_empty.tar.gz
new file mode 100644
index 00000000000..5c2afa1a8f6
--- /dev/null
+++ b/spec/fixtures/pages_empty.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip b/spec/fixtures/pages_empty.zip
new file mode 100644
index 00000000000..db3f0334c12
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip.meta b/spec/fixtures/pages_empty.zip.meta
new file mode 100644
index 00000000000..d0b93b3b9c0
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip.meta
Binary files differ
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 20241d4d63e..5231ab0ba3f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -192,6 +192,7 @@ project:
- environments
- deployments
- project_feature
+- pages_domains
- authorized_users
- project_authorizations
- route
diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb
index 4092f7fb638..e2d6b1b9ab7 100644
--- a/spec/lib/gitlab/uploads_transfer_spec.rb
+++ b/spec/lib/gitlab/project_transfer_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
-describe Gitlab::UploadsTransfer, lib: true do
+describe Gitlab::ProjectTransfer, lib: true do
before do
@root_dir = File.join(Rails.root, "public", "uploads")
- @upload_transfer = Gitlab::UploadsTransfer.new
+ @project_transfer = Gitlab::ProjectTransfer.new
+ allow(@project_transfer).to receive(:root_dir).and_return(@root_dir)
@project_path_was = "test_project_was"
@project_path = "test_project"
@@ -21,7 +22,7 @@ describe Gitlab::UploadsTransfer, lib: true do
describe '#move_project' do
it "moves project upload to another namespace" do
FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
+ @project_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
expected_path = File.join(@root_dir, @namespace_path, @project_path)
expect(Dir.exist?(expected_path)).to be_truthy
@@ -31,7 +32,7 @@ describe Gitlab::UploadsTransfer, lib: true do
describe '#rename_project' do
it "renames project" do
FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
- @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
+ @project_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
expected_path = File.join(@root_dir, @namespace_path, @project_path)
expect(Dir.exist?(expected_path)).to be_truthy
@@ -41,7 +42,7 @@ describe Gitlab::UploadsTransfer, lib: true do
describe '#rename_namespace' do
it "renames namespace" do
FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path)
+ @project_transfer.rename_namespace(@namespace_path_was, @namespace_path)
expected_path = File.join(@root_dir, @namespace_path, @project_path)
expect(Dir.exist?(expected_path)).to be_truthy
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
new file mode 100644
index 00000000000..e6a4583a8fb
--- /dev/null
+++ b/spec/models/pages_domain_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe PagesDomain, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe :validate_domain do
+ subject { build(:pages_domain, domain: domain) }
+
+ context 'is unique' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to validate_uniqueness_of(:domain) }
+ end
+
+ context 'valid domain' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'valid hexadecimal-looking domain' do
+ let(:domain) { '0x12345.com'}
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'no domain' do
+ let(:domain) { nil }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'invalid domain' do
+ let(:domain) { '0123123' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'domain from .example.com' do
+ let(:domain) { 'my.domain.com' }
+
+ before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe 'validate certificate' do
+ subject { domain }
+
+ context 'when only certificate is specified' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when only key is specified' do
+ let(:domain) { build(:pages_domain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'for not matching key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe :url do
+ subject { domain.url }
+
+ context 'without the certificate' do
+ let(:domain) { build(:pages_domain) }
+
+ it { is_expected.to eq('http://my.domain.com') }
+ end
+
+ context 'with a certificate' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to eq('https://my.domain.com') }
+ end
+ end
+
+ describe :has_matching_key? do
+ subject { domain.has_matching_key? }
+
+ context 'for matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for invalid key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe :has_intermediates? do
+ subject { domain.has_intermediates? }
+
+ context 'for self signed' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for missing certificate chain' do
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for trusted certificate chain' do
+ # We only validate that we can to rebuild the trust chain, for certificates
+ # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store.
+ # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine
+
+ let(:domain) { build(:pages_domain, :with_trusted_chain) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :expired? do
+ subject { domain.expired? }
+
+ context 'for valid' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for expired' do
+ let(:domain) { build(:pages_domain, :with_expired_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :subject do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.subject }
+
+ it { is_expected.to eq('/CN=test-certificate') }
+ end
+
+ describe :certificate_text do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.certificate_text }
+
+ # We test only existence of output, since the output is long
+ it { is_expected.not_to be_empty }
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 48b085781e7..591ea314142 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -60,6 +60,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:pages_domains) }
it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
@@ -1067,6 +1068,22 @@ describe Project, models: true do
end
end
+ describe '#pages_deployed?' do
+ let(:project) { create :empty_project }
+
+ subject { project.pages_deployed? }
+
+ context 'if public folder does exist' do
+ before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "if public folder doesn't exist" do
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '.search' do
let(:project) { create(:empty_project, description: 'kitten mittens') }
@@ -1844,4 +1861,31 @@ describe Project, models: true do
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
+
+ describe '#pages_url' do
+ let(:group) { create :group, name: group_name }
+ let(:project) { create :empty_project, namespace: group, name: project_name }
+ let(:domain) { 'Example.com' }
+
+ subject { project.pages_url }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
+ end
+
+ context 'group page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'group.example.com' }
+
+ it { is_expected.to eq("http://group.example.com") }
+ end
+
+ context 'project page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'Project' }
+
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
+ end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 96889abee79..a5bc62ef6c2 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -27,35 +27,42 @@ describe 'project routing' do
# let(:actions) { [:index] }
# let(:controller) { 'issues' }
# end
+ #
+ # # Different controller name and path
+ # it_behaves_like 'RESTful project resources' do
+ # let(:controller) { 'pages_domains' }
+ # let(:controller_path) { 'pages/domains' }
+ # end
shared_examples 'RESTful project resources' do
let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
+ let(:controller_path) { controller }
it 'to #index' do
- expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
+ expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
end
it 'to #create' do
- expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
+ expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
end
it 'to #new' do
- expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
end
it 'to #edit' do
- expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
end
it 'to #show' do
- expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
end
it 'to #update' do
- expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
+ expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
end
it 'to #destroy' do
- expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
+ expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
end
end
@@ -539,4 +546,20 @@ describe 'project routing' do
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
+
+ describe Projects::PagesDomainsController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:show, :new, :create, :destroy] }
+ let(:controller) { 'pages_domains' }
+ let(:controller_path) { 'pages/domains' }
+ end
+
+ it 'to #destroy with a valid domain name' do
+ expect(delete('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+
+ it 'to #show with a valid domain' do
+ expect(get('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+ end
end
diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb
new file mode 100644
index 00000000000..aa63fe3a5c1
--- /dev/null
+++ b/spec/services/pages_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe PagesService, services: true do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:service) { PagesService.new(data) }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ context 'execute asynchronously for pages job' do
+ before { build.name = 'pages' }
+
+ context 'on success' do
+ before { build.success }
+
+ it 'executes worker' do
+ expect(PagesWorker).to receive(:perform_async)
+ service.execute
+ end
+ end
+
+ %w(pending running failed canceled).each do |status|
+ context "on #{status}" do
+ before { build.status = status }
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+ end
+ end
+
+ context 'for other jobs' do
+ before do
+ build.name = 'other job'
+ build.success
+ end
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 1540b90163a..5d5812c2c15 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -9,6 +9,8 @@ describe Projects::TransferService, services: true do
before do
allow_any_instance_of(Gitlab::UploadsTransfer).
to receive(:move_project).and_return(true)
+ allow_any_instance_of(Gitlab::PagesTransfer).
+ to receive(:move_project).and_return(true)
group.add_owner(user)
@result = transfer_project(project, user, group)
end
diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb
new file mode 100644
index 00000000000..8b329bc21c3
--- /dev/null
+++ b/spec/services/projects/update_pages_configuration_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Projects::UpdatePagesConfigurationService, services: true do
+ let(:project) { create(:empty_project) }
+ subject { described_class.new(project) }
+
+ describe "#update" do
+ let(:file) { Tempfile.new('pages-test') }
+
+ after do
+ file.close
+ file.unlink
+ end
+
+ it 'updates the .update file' do
+ # Access this reference to ensure scoping works
+ Projects::Settings # rubocop:disable Lint/Void
+ expect(subject).to receive(:pages_config_file).and_return(file.path)
+ expect(subject).to receive(:reload_daemon).and_call_original
+
+ expect(subject.execute).to eq({ status: :success })
+ end
+ end
+end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
new file mode 100644
index 00000000000..411b22a0fb8
--- /dev/null
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -0,0 +1,80 @@
+require "spec_helper"
+
+describe Projects::UpdatePagesService do
+ let(:project) { create :project }
+ let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha }
+ let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' }
+ let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
+
+ subject { described_class.new(project, build) }
+
+ before do
+ project.remove_pages
+ end
+
+ %w(tar.gz zip).each do |format|
+ context "for valid #{format}" do
+ let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") }
+ let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") }
+ let(:metadata) do
+ filename = Rails.root + "spec/fixtures/pages.#{format}.meta"
+ fixture_file_upload(filename) if File.exist?(filename)
+ end
+
+ before do
+ build.update_attributes(artifacts_file: file)
+ build.update_attributes(artifacts_metadata: metadata)
+ end
+
+ it 'succeeds' do
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ end
+
+ it 'limits pages size' do
+ stub_application_setting(max_pages_size: 1)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'removes pages after destroy' do
+ expect(PagesWorker).to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ project.destroy
+ expect(project.pages_deployed?).to be_falsey
+ end
+
+ it 'fails if sha on branch is not latest' do
+ pipeline.update_attributes(sha: 'old_sha')
+ build.update_attributes(artifacts_file: file)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for empty file fails' do
+ build.update_attributes(artifacts_file: empty_file)
+ expect(execute).not_to eq(:success)
+ end
+ end
+ end
+
+ it 'fails to remove project pages when no pages is deployed' do
+ expect(PagesWorker).not_to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ project.destroy
+ end
+
+ it 'fails if no artifacts' do
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for invalid archive' do
+ build.update_attributes(artifacts_file: invalid_file)
+ expect(execute).not_to eq(:success)
+ end
+
+ def execute
+ subject.execute[:status]
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index bc751d20ce1..df8a47893f9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -28,7 +28,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
+ %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -71,6 +71,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
@@ -202,7 +203,7 @@ describe 'gitlab:app namespace rake task' do
it 'sets correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
@@ -210,14 +211,15 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('repositories/')
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end
it 'deletes temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
)
expect(temp_dirs).to be_empty
@@ -304,7 +306,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -312,6 +314,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -327,6 +330,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke