Commit 3b82b4f7 authored by Alishan Ladhani's avatar Alishan Ladhani Committed by Fatih Acet

Create serverless domains controller

To be used for setting the instance serverless domain in the admin panel
parent 76c5e52e
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
const domainCard = document.querySelector('.js-domain-cert-show');
const domainForm = document.querySelector('.js-domain-cert-inputs');
const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
if (domainReplaceButton && domainCard && domainForm) {
domainReplaceButton.addEventListener('click', () => {
domainCard.classList.add('hidden');
domainForm.classList.remove('hidden');
domainSubmitButton.removeAttribute('disabled');
});
}
});
......@@ -56,4 +56,15 @@
border-top-right-radius: $border-radius-default;
}
&.floating-status-badge {
position: absolute;
right: $gl-padding-24;
bottom: $gl-padding-4;
margin-bottom: 0;
}
}
.form-control.has-floating-status-badge {
position: relative;
padding-right: 120px;
}
# frozen_string_literal: true
class Admin::Serverless::DomainsController < Admin::ApplicationController
before_action :check_feature_flag
before_action :domain, only: [:update, :verify]
def index
@domain = PagesDomain.instance_serverless.first_or_initialize
end
def create
if PagesDomain.instance_serverless.count > 0
return redirect_to admin_serverless_domains_path, notice: _('An instance-level serverless domain already exists.')
end
@domain = PagesDomain.instance_serverless.create(create_params)
if @domain.persisted?
redirect_to admin_serverless_domains_path, notice: _('Domain was successfully created.')
else
render 'index'
end
end
def update
if domain.update(update_params)
redirect_to admin_serverless_domains_path, notice: _('Domain was successfully updated.')
else
render 'index'
end
end
def verify
result = VerifyPagesDomainService.new(domain).execute
if result[:status] == :success
flash[:notice] = _('Successfully verified domain ownership')
else
flash[:alert] = _('Failed to verify domain ownership')
end
redirect_to admin_serverless_domains_path
end
private
def domain
@domain = PagesDomain.instance_serverless.find(params[:id])
end
def check_feature_flag
render_404 unless Feature.enabled?(:serverless_domain)
end
def update_params
params.require(:pages_domain).permit(:user_provided_certificate, :user_provided_key)
end
def create_params
params.require(:pages_domain).permit(:domain, :user_provided_certificate, :user_provided_key)
end
end
......@@ -14,6 +14,7 @@ class PagesDomain < ApplicationRecord
validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, :key, presence: true, if: :usage_serverless?
validates :certificate, presence: { message: 'must be present if HTTPS-only is enabled' },
if: :certificate_should_be_present?
validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? }
......@@ -64,6 +65,8 @@ class PagesDomain < ApplicationRecord
scope :with_logging_info, -> { includes(project: [:namespace, :route]) }
scope :instance_serverless, -> { where(wildcard: true, scope: :instance, usage: :serverless) }
def verified?
!!verified_at
end
......
- form_name = 'js-serverless-domain-settings'
- form_url = @domain.persisted? ? admin_serverless_domain_path(@domain.id, anchor: form_name) : admin_serverless_domains_path(anchor: form_name)
- show_certificate_card = @domain.persisted? && @domain.errors.blank?
= form_for @domain, url: form_url, html: { class: 'fieldset-form' } do |f|
= form_errors(@domain)
%fieldset
- if @domain.persisted?
- dns_record = "*.#{@domain.domain} CNAME #{Settings.pages.host}."
- verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
.form-group.row
.col-sm-6.position-relative
= f.label :domain, _('Domain'), class: 'label-bold'
= f.text_field :domain, class: 'form-control has-floating-status-badge', readonly: true
.status-badge.floating-status-badge
- text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
.badge{ class: status }
= text
= link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "btn has-tooltip", title: _("Retry verification")
.col-sm-6
= f.label :serverless_domain_dns, _('DNS'), class: 'label-bold'
.input-group
= text_field_tag :serverless_domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#serverless_domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
.col-sm-12.form-text.text-muted
= _("To access this domain create a new DNS record")
.form-group
= f.label :serverless_domain_verification, _('Verification status'), class: 'label-bold'
.input-group
= text_field_tag :serverless_domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#serverless_domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
= _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help }
- else
.form-group
= f.label :domain, _('Domain'), class: 'label-bold'
= f.text_field :domain, class: 'form-control'
- if show_certificate_card
.card.js-domain-cert-show
.card-header
= _('Certificate')
.d-flex.justify-content-between.align-items-center.p-3
%span
= @domain.subject || _('missing')
%button.btn.btn-remove.btn-sm.js-domain-cert-replace-btn{ type: 'button' }
= _('Replace')
.js-domain-cert-inputs{ class: ('hidden' if show_certificate_card) }
.form-group
= f.label :user_provided_certificate, _('Certificate (PEM)'), class: 'label-bold'
= f.text_area :user_provided_certificate, rows: 5, class: 'form-control', value: ''
%span.form-text.text-muted
= _("Upload a certificate for your domain with all intermediates")
.form-group
= f.label :user_provided_key, _('Key (PEM)'), class: 'label-bold'
= f.text_area :user_provided_key, rows: 5, class: 'form-control', value: ''
%span.form-text.text-muted
= _("Upload a private key for your certificate")
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "btn btn-success js-serverless-domain-submit", disabled: @domain.persisted?
- breadcrumb_title _("Operations")
- page_title _("Operations")
- @content_class = "limit-container-width" unless fluid_layout
-# normally expanded_by_default? is used here, but since this is the only panel
-# in this settings page, let's leave it always open by default
- expanded = true
%section.settings.as-serverless-domain.no-animate#js-serverless-domain-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Serverless domain')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set an instance-wide domain that will be available to all clusters when installing Knative.')
.settings-content
- if Gitlab.config.pages.enabled
= render 'form'
- else
.card
.card-header
= s_('GitLabPages|Domains')
.nothing-here-block
= s_("GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it.")
......@@ -254,6 +254,11 @@
= link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do
%span
= _('CI/CD')
- if Feature.enabled?(:serverless_domain)
= nav_link(path: 'application_settings#operations') do
= link_to admin_serverless_domains_path, title: _('Operations') do
%span
= _('Operations')
= nav_link(path: 'application_settings#reporting') do
= link_to reporting_admin_application_settings_path, title: _('Reporting') do
%span
......
---
title: Add admin settings panel for instance-level serverless domain (behind feature flag)
merge_request: 21222
author:
type: added
......@@ -32,6 +32,14 @@ namespace :admin do
resources :abuse_reports, only: [:index, :destroy]
resources :gitaly_servers, only: [:index]
namespace :serverless do
resources :domains, only: [:index, :create, :update] do
member do
post '/verify', to: 'domains#verify'
end
end
end
resources :spam_logs, only: [:index, :destroy] do
member do
post :mark_as_ham
......
......@@ -1107,6 +1107,9 @@ msgstr ""
msgid "Add comment now"
msgstr ""
msgid "Add domain"
msgstr ""
msgid "Add email address"
msgstr ""
......@@ -1919,6 +1922,9 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
msgid "An instance-level serverless domain already exists."
msgstr ""
msgid "An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable."
msgstr ""
......@@ -6807,6 +6813,12 @@ msgstr ""
msgid "Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled"
msgstr ""
msgid "Domain was successfully created."
msgstr ""
msgid "Domain was successfully updated."
msgstr ""
msgid "Don't have an account yet?"
msgstr ""
......@@ -8184,6 +8196,9 @@ msgstr ""
msgid "Failed to upload object map file"
msgstr ""
msgid "Failed to verify domain ownership"
msgstr ""
msgid "Failure"
msgstr ""
......@@ -17232,6 +17247,9 @@ msgstr ""
msgid "Serverless"
msgstr ""
msgid "Serverless domain"
msgstr ""
msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first."
msgstr ""
......@@ -17337,6 +17355,9 @@ msgstr ""
msgid "Set a template repository for projects in this group"
msgstr ""
msgid "Set an instance-wide domain that will be available to all clusters when installing Knative."
msgstr ""
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
msgstr ""
......@@ -18551,6 +18572,9 @@ msgstr ""
msgid "Successfully unlocked"
msgstr ""
msgid "Successfully verified domain ownership"
msgstr ""
msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe Admin::Serverless::DomainsController do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
describe '#index' do
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
get :index
expect(response.status).to eq(404)
end
end
context 'admin user' do
before do
create(:pages_domain)
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
get :index
expect(response.status).to eq(404)
end
end
context 'when instance-level serverless domain exists' do
let!(:serverless_domain) { create(:pages_domain, :instance_serverless) }
it 'loads the instance serverless domain' do
get :index
expect(assigns(:domain).id).to eq(serverless_domain.id)
end
end
context 'when domain does not exist' do
it 'initializes an instance serverless domain' do
get :index
domain = assigns(:domain)
expect(domain.persisted?).to eq(false)
expect(domain.wildcard).to eq(true)
expect(domain.scope).to eq('instance')
expect(domain.usage).to eq('serverless')
end
end
end
end
describe '#create' do
let(:create_params) do
sample_domain = build(:pages_domain)
{
domain: 'serverless.gitlab.io',
user_provided_certificate: sample_domain.certificate,
user_provided_key: sample_domain.key
}
end
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
post :create, params: { pages_domain: create_params }
expect(response.status).to eq(404)
end
end
context 'admin user' do
before do
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
post :create, params: { pages_domain: create_params }
expect(response.status).to eq(404)
end
end
context 'when an instance-level serverless domain exists' do
let!(:serverless_domain) { create(:pages_domain, :instance_serverless) }
it 'does not create a new domain' do
expect { post :create, params: { pages_domain: create_params } }.not_to change { PagesDomain.instance_serverless.count }
end
it 'redirects to index' do
post :create, params: { pages_domain: create_params }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to include('An instance-level serverless domain already exists.')
end
end
context 'when an instance-level serverless domain does not exist' do
it 'creates an instance serverless domain with the provided attributes' do
expect { post :create, params: { pages_domain: create_params } }.to change { PagesDomain.instance_serverless.count }.by(1)
domain = PagesDomain.instance_serverless.first
expect(domain.domain).to eq(create_params[:domain])
expect(domain.certificate).to eq(create_params[:user_provided_certificate])
expect(domain.key).to eq(create_params[:user_provided_key])
expect(domain.wildcard).to eq(true)
expect(domain.scope).to eq('instance')
expect(domain.usage).to eq('serverless')
end
it 'redirects to index' do
post :create, params: { pages_domain: create_params }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to include('Domain was successfully created.')
end
end
context 'when there are errors' do
it 'renders index view' do
post :create, params: { pages_domain: { foo: 'bar' } }
expect(assigns(:domain).errors.size).to be > 0
expect(response).to render_template('index')
end
end
end
end
describe '#update' do
let(:domain) { create(:pages_domain, :instance_serverless) }
let(:update_params) do
sample_domain = build(:pages_domain)
{
user_provided_certificate: sample_domain.certificate,
user_provided_key: sample_domain.key
}
end
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
put :update, params: { id: domain.id, pages_domain: update_params }
expect(response.status).to eq(404)
end
end
context 'admin user' do
before do
sign_in(admin)
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
put :update, params: { id: domain.id, pages_domain: update_params }
expect(response.status).to eq(404)
end
end
context 'when domain exists' do
it 'updates the domain with the provided attributes' do
new_certificate = build(:pages_domain, :ecdsa).certificate
new_key = build(:pages_domain, :ecdsa).key
put :update, params: { id: domain.id, pages_domain: { user_provided_certificate: new_certificate, user_provided_key: new_key } }
domain.reload
expect(domain.certificate).to eq(new_certificate)
expect(domain.key).to eq(new_key)
end
it 'does not update the domain name' do
put :update, params: { id: domain.id, pages_domain: { domain: 'new.com' } }
expect(domain.reload.domain).not_to eq('new.com')
end
it 'redirects to index' do
put :update, params: { id: domain.id, pages_domain: update_params }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to include('Domain was successfully updated.')
end
end
context 'when domain does not exist' do
it 'returns 404' do
put :update, params: { id: 0, pages_domain: update_params }
expect(response.status).to eq(404)
end
end
context 'when there are errors' do
it 'renders index view' do
put :update, params: { id: domain.id, pages_domain: { user_provided_certificate: 'bad certificate' } }
expect(assigns(:domain).errors.size).to be > 0
expect(response).to render_template('index')
end
end
end
end
describe '#verify' do
let(:domain) { create(:pages_domain, :instance_serverless) }
context 'non-admin user' do
before do
sign_in(user)
end
it 'responds with 404' do
post :verify, params: { id: domain.id }
expect(response.status).to eq(404)
end
end
context 'admin user' do
before do
sign_in(admin)
end
def stub_service
service = double(:service)
expect(VerifyPagesDomainService).to receive(:new).with(domain).and_return(service)
service
end
context 'with serverless_domain feature disabled' do
before do
stub_feature_flags(serverless_domain: false)
end
it 'responds with 404' do
post :verify, params: { id: domain.id }
expect(response.status).to eq(404)
end
end
it 'handles verification success' do
expect(stub_service).to receive(:execute).and_return(status: :success)
post :verify, params: { id: domain.id }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:notice]).to eq('Successfully verified domain ownership')
end
it 'handles verification failure' do
expect(stub_service).to receive(:execute).and_return(status: :failed)
post :verify, params: { id: domain.id }
expect(response).to redirect_to admin_serverless_domains_path
expect(flash[:alert]).to eq('Failed to verify domain ownership')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Admin Serverless Domains', :js do
let(:sample_domain) { build(:pages_domain) }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
sign_in(create(:admin))
end
it 'Add domain with certificate' do
visit admin_serverless_domains_path
fill_in 'pages_domain[domain]', with: 'foo.com'
fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate
fill_in 'pages_domain[user_provided_key]', with: sample_domain.key
click_button 'Add domain'
expect(current_path).to eq admin_serverless_domains_path
expect(page).to have_field('pages_domain[domain]', with: 'foo.com')
expect(page).to have_field('serverless_domain_dns', with: /^\*\.foo\.com CNAME /)
expect(page).to have_field('serverless_domain_verification', with: /^_gitlab-pages-verification-code.foo.com TXT /)
expect(page).not_to have_field('pages_domain[user_provided_certificate]')
expect(page).not_to have_field('pages_domain[user_provided_key]')
expect(page).to have_content 'Unverified'
expect(page).to have_content '/CN=test-certificate'
end
it 'Update domain certificate' do
visit admin_serverless_domains_path
fill_in 'pages_domain[domain]', with: 'foo.com'
fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate
fill_in 'pages_domain[user_provided_key]', with: sample_domain.key
click_button 'Add domain'
expect(current_path).to eq admin_serverless_domains_path
expect(page).not_to have_field('pages_domain[user_provided_certificate]')
expect(page).not_to have_field('pages_domain[user_provided_key]')
click_button 'Replace'
expect(page).to have_field('pages_domain[user_provided_certificate]')
expect(page).to have_field('pages_domain[user_provided_key]')
fill_in 'pages_domain[user_provided_certificate]', with: sample_domain.certificate
fill_in 'pages_domain[user_provided_key]', with: sample_domain.key
click_button 'Save changes'
expect(page).to have_content 'Domain was successfully updated'
expect(page).to have_content '/CN=test-certificate'
end
end
......@@ -104,6 +104,14 @@ describe PagesDomain do
describe 'validate certificate' do
subject { domain }
context 'serverless domain' do
it 'requires certificate and key to be present' do
expect(build(:pages_domain, :without_certificate, :without_key, usage: :serverless)).not_to be_valid
expect(build(:pages_domain, :without_certificate, usage: :serverless)).not_to be_valid
expect(build(:pages_domain, :without_key, usage: :serverless)).not_to be_valid
end
end
context 'with matching key' do
let(:domain) { build(:pages_domain) }
......@@ -555,6 +563,28 @@ describe PagesDomain do
end
end
describe '.instance_serverless' do
subject { described_class.instance_serverless }
before do
create(:pages_domain, wildcard: true)
create(:pages_domain, :instance_serverless)
create(:pages_domain, scope: :instance)
create(:pages_domain, :instance_serverless)
create(:pages_domain, usage: :serverless)
end
it 'returns domains that are wildcard, instance-level, and serverless' do
expect(subject.length).to eq(2)
subject.each do |domain|
expect(domain.wildcard).to eq(true)
expect(domain.usage).to eq('serverless')
expect(domain.scope).to eq('instance')
end
end
end
describe '.need_auto_ssl_renewal' do
subject { described_class.need_auto_ssl_renewal }
......
# frozen_string_literal: true
require 'spec_helper'
describe Admin::Serverless::DomainsController do
it 'routes to #index' do
expect(get: '/admin/serverless/domains').to route_to('admin/serverless/domains#index')
end
it 'routes to #create' do
expect(post: '/admin/serverless/domains/').to route_to('admin/serverless/domains#create')
end
it 'routes to #update' do
expect(put: '/admin/serverless/domains/1').to route_to(controller: 'admin/serverless/domains', action: 'update', id: '1')
expect(patch: '/admin/serverless/domains/1').to route_to(controller: 'admin/serverless/domains', action: 'update', id: '1')
end
it 'routes #verify' do
expect(post: '/admin/serverless/domains/1/verify').to route_to(controller: 'admin/serverless/domains', action: 'verify', id: '1')
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment