Commit 7edeea0d authored by Douwe Maan's avatar Douwe Maan

Merge branch 'fj-5426-web-ide-terminal' into 'master'

Web terminal from attached CI runner in Web IDE

See merge request gitlab-org/gitlab-ee!7386
parents add85ce1 dfd4ec0e
......@@ -9,7 +9,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project'
......
# frozen_string_literal: true
class Projects::WebIdeTerminalsController < Projects::ApplicationController
before_action :authenticate_user!
before_action :build, except: [:check_config, :create]
before_action :authorize_create_web_ide_terminal!
before_action :authorize_read_web_ide_terminal!, except: [:check_config, :create]
before_action :authorize_update_web_ide_terminal!, only: [:cancel, :retry]
def check_config
return respond_422 unless branch_sha
result = ::Ci::WebIdeConfigService.new(project, current_user, sha: branch_sha).execute
if result[:status] == :success
head :ok
else
respond_422
end
end
def show
render_terminal(build)
end
def create
result = ::Ci::CreateWebIdeTerminalService.new(project,
current_user,
ref: params[:branch])
.execute
if result[:status] == :error
render status: :bad_request, json: result[:message]
else
pipeline = result[:pipeline]
current_build = pipeline.builds.last
if current_build
render_terminal(current_build)
else
render status: :bad_request, json: pipeline.errors.full_messages
end
end
end
def cancel
return respond_422 unless build.cancelable?
build.cancel
head :ok
end
def retry
return respond_422 unless build.retryable?
new_build = Ci::Build.retry(build, current_user)
render_terminal(new_build)
end
private
def authorize_create_web_ide_terminal!
return access_denied! unless can?(current_user, :create_web_ide_terminal, project)
end
def authorize_read_web_ide_terminal!
authorize_build_ability!(:read_web_ide_terminal)
end
def authorize_update_web_ide_terminal!
authorize_build_ability!(:update_web_ide_terminal)
end
def authorize_build_ability!(ability)
return access_denied! unless can?(current_user, ability, build)
end
def build
@build ||= project.builds.find(params[:id])
end
def branch_sha
return unless params[:branch].present?
project.commit(params[:branch])&.id
end
def render_terminal(current_build)
render json: WebIdeTerminalSerializer
.new(project: project, current_user: current_user)
.represent(current_build)
end
end
......@@ -92,7 +92,7 @@ class License < ActiveRecord::Base
prometheus_alerts
operations_dashboard
tracing
webide_terminal
web_ide_terminal
].freeze
# List all features available for early adopters,
......
# frozen_string_literal: true
class WebIdeTerminal
include ::Gitlab::Routing
attr_reader :build, :project
delegate :id, :status, to: :build
def initialize(build)
@build = build
@project = build.project
end
def show_path
web_ide_terminal_route_generator(:show)
end
def retry_path
web_ide_terminal_route_generator(:retry)
end
def cancel_path
web_ide_terminal_route_generator(:cancel)
end
def terminal_path
terminal_project_job_path(project, build, format: :ws)
end
private
def web_ide_terminal_route_generator(action)
url_for(action: action,
controller: 'projects/web_ide_terminals',
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: build.id,
only_path: true)
end
end
......@@ -11,6 +11,23 @@ module EE
prevent :update_build
end
condition(:is_web_ide_terminal, scope: :subject) do
@subject.pipeline.webide?
end
rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
enable :read_web_ide_terminal
enable :update_web_ide_terminal
end
rule { is_web_ide_terminal & ~can?(:update_web_ide_terminal) }.policy do
prevent :create_build_terminal
end
rule { can?(:update_web_ide_terminal) & terminal }.policy do
enable :create_build_terminal
end
private
alias_method :current_user, :user
......
......@@ -203,7 +203,7 @@ module EE
end
condition(:web_ide_terminal_available) do
@subject.feature_available?(:webide_terminal)
@subject.feature_available?(:web_ide_terminal)
end
rule { web_ide_terminal_available & can?(:create_pipeline) & can?(:maintainer_access) }.enable :create_web_ide_terminal
......
# frozen_string_literal: true
class WebIdeTerminalEntity < Grape::Entity
expose :id
expose :status
expose :show_path
expose :cancel_path
expose :retry_path
expose :terminal_path
end
# frozen_string_literal: true
class WebIdeTerminalSerializer < BaseSerializer
entity WebIdeTerminalEntity
def represent(resource, opts = {})
resource = WebIdeTerminal.new(resource) if resource.is_a?(Ci::Build)
super
end
end
# frozen_string_literal: true
module Ci
class CreateWebideTerminalService < ::BaseService
class CreateWebIdeTerminalService < ::BaseService
include ::Gitlab::Utils::StrongMemoize
TerminalCreationError = Class.new(StandardError)
......@@ -62,7 +62,7 @@ module Ci
end
def load_terminal_config!
result = ::Ci::WebideConfigService.new(project, current_user, sha: sha).execute
result = ::Ci::WebIdeConfigService.new(project, current_user, sha: sha).execute
raise TerminalCreationError, result[:message] if result[:status] != :success
@terminal = result[:terminal]
......
# frozen_string_literal: true
module Ci
class WebideConfigService < ::BaseService
class WebIdeConfigService < ::BaseService
include ::Gitlab::Utils::StrongMemoize
ValidationError = Class.new(StandardError)
......@@ -37,12 +37,12 @@ module Ci
end
def load_config!
@config = Gitlab::Webide::Config.new(config_content)
@config = Gitlab::WebIde::Config.new(config_content)
unless @config.valid?
raise ValidationError, @config.errors.first
end
rescue Gitlab::Webide::Config::ConfigError => e
rescue Gitlab::WebIde::Config::ConfigError => e
raise ValidationError, e.message
end
......
---
title: Added web terminals to Web IDE
merge_request: 7386
author:
type: added
......@@ -20,6 +20,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'epics'
end
end
resources :web_ide_terminals, path: :ide_terminals, only: [:create, :show], constraints: { id: /\d+/, format: :json } do
member do
post :cancel
post :retry
end
collection do
post :check_config
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Webide
module WebIde
#
# Base GitLab Webide Configuration facade
# Base GitLab WebIde Configuration facade
#
class Config
ConfigError = Class.new(StandardError)
......
# frozen_string_literal: true
module Gitlab
module Webide
module WebIde
class Config
module Entry
##
# This class represents a global entry - root Entry for entire
# GitLab Webide Configuration file.
# GitLab WebIde Configuration file.
#
class Global < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
......
# frozen_string_literal: true
module Gitlab
module Webide
module WebIde
class Config
module Entry
##
......
......@@ -2,13 +2,13 @@
require 'spec_helper'
describe Ci::CreateWebideTerminalService do
describe Ci::CreateWebIdeTerminalService do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:ref) { 'master' }
before do
stub_licensed_features(webide_terminal: true)
stub_licensed_features(web_ide_terminal: true)
end
describe '#execute' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Ci::WebideConfigService do
describe Ci::WebIdeConfigService do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:sha) { 'sha' }
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::WebIdeTerminalsController do
let(:owner) { create(:owner) }
let(:admin) { create(:admin) }
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project, :private, :repository, namespace: owner.namespace) }
let(:pipeline) { create(:ci_pipeline, project: project, source: :webide, config_source: :webide_source, user: user) }
let(:job) { create(:ci_build, pipeline: pipeline, user: user, project: project) }
let(:user) { maintainer }
before do
stub_licensed_features(web_ide_terminal: true)
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest)
sign_in(user)
end
shared_examples 'terminal access rights' do
context 'with admin' do
let(:user) { admin }
it 'returns 200' do
expect(response).to have_gitlab_http_status(200)
end
end
context 'with owner' do
let(:user) { owner }
it 'returns 200' do
expect(response).to have_gitlab_http_status(200)
end
end
context 'with maintainer' do
let(:user) { maintainer }
it 'returns 200' do
expect(response).to have_gitlab_http_status(200)
end
end
context 'with developer' do
let(:user) { developer }
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
end
end
context 'with reporter' do
let(:user) { reporter }
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
end
end
context 'with guest' do
let(:user) { guest }
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
end
end
context 'with non member' do
let(:user) { create(:user) }
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
end
end
end
shared_examples 'when pipeline is not from a webide source' do
context 'with admin' do
let(:user) { admin }
let(:pipeline) { create(:ci_pipeline, project: project, source: :chat, user: user) }
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'GET show' do
before do
get(:show, namespace_id: project.namespace.to_param, project_id: project, id: job.id)
end
it_behaves_like 'terminal access rights'
it_behaves_like 'when pipeline is not from a webide source'
end
describe 'POST check_config' do
let(:result) { { status: :success } }
before do
allow_any_instance_of(::Ci::WebIdeConfigService)
.to receive(:execute).and_return(result)
post :check_config, namespace_id: project.namespace.to_param,
project_id: project.to_param,
branch: 'master'
end
it_behaves_like 'terminal access rights'
context 'when invalid config file' do
let(:user) { admin }
let(:result) { { status: :error } }
it 'returns 422' do
expect(response).to have_gitlab_http_status(422)
end
end
end
describe 'POST create' do
let(:branch) { 'master' }
subject do
post :create, namespace_id: project.namespace.to_param,
project_id: project.to_param,
branch: branch
end
context 'access rights' do
let(:build) { create(:ci_build, project: project) }
let(:pipeline) { build.pipeline }
before do
allow_any_instance_of(::Ci::CreateWebIdeTerminalService)
.to receive(:execute).and_return(status: :success, pipeline: pipeline)
subject
end
it_behaves_like 'terminal access rights'
end
context 'when branch does not exist' do
let(:user) { admin }
let(:branch) { 'foobar' }
it 'returns 400' do
subject
expect(response).to have_gitlab_http_status(400)
end
end
context 'when there is an error creating the job' do
let(:user) { admin }
it 'returns 400' do
allow_any_instance_of(::Ci::CreateWebIdeTerminalService)
.to receive(:execute).and_return(status: :error, message: 'foobar')
subject
expect(response).to have_gitlab_http_status(400)
end
end
end
describe 'POST cancel' do
let(:job) { create(:ci_build, :running, pipeline: pipeline, user: user, project: project) }
before do
post(:cancel, namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: job.id)
end
it_behaves_like 'terminal access rights'
it_behaves_like 'when pipeline is not from a webide source'
context 'when job is not cancelable' do
let!(:job) { create(:ci_build, :failed, pipeline: pipeline, user: user) }
it 'returns 422' do
expect(response).to have_gitlab_http_status(422)
end
end
end
describe 'POST retry' do
let(:job) { create(:ci_build, :failed, pipeline: pipeline, user: user, project: project) }
before do
post(:retry, namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: job.id)
end
it_behaves_like 'terminal access rights'
it_behaves_like 'when pipeline is not from a webide source'
context 'when job is not retryable' do
let!(:job) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'returns 422' do
expect(response).to have_gitlab_http_status(422)
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Webide::Config::Entry::Global do
describe Gitlab::WebIde::Config::Entry::Global do
let(:global) { described_class.new(hash) }
describe '.nodes' do
......@@ -39,7 +39,7 @@ describe Gitlab::Webide::Config::Entry::Global do
it 'creates node object using valid class' do
expect(global.descendants.first)
.to be_an_instance_of Gitlab::Webide::Config::Entry::Terminal
.to be_an_instance_of Gitlab::WebIde::Config::Entry::Terminal
end
it 'sets correct description for nodes' do
......@@ -149,7 +149,7 @@ describe Gitlab::Webide::Config::Entry::Global do
context 'when entry exists' do
it 'returns correct entry' do
expect(global[:terminal])
.to be_an_instance_of Gitlab::Webide::Config::Entry::Terminal
.to be_an_instance_of Gitlab::WebIde::Config::Entry::Terminal
expect(global[:terminal][:before_script].value).to eq ['ls']
end
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Webide::Config::Entry::Terminal do
describe Gitlab::WebIde::Config::Entry::Terminal do
let(:entry) { described_class.new(config) }
describe '.nodes' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Gitlab::Webide::Config do
describe Gitlab::WebIde::Config do
let(:config) do
described_class.new(yml)
end
......
# frozen_string_literal: true
require 'spec_helper'
describe WebIdeTerminal do
let(:build) { create(:ci_build) }
subject { described_class.new(build) }
it 'returns the show_path of the build' do
expect(subject.show_path).to end_with("/ide_terminals/#{build.id}")
end
it 'returns the retry_path of the build' do
expect(subject.retry_path).to end_with("/ide_terminals/#{build.id}/retry")
end
it 'returns the cancel_path of the build' do
expect(subject.cancel_path).to end_with("/ide_terminals/#{build.id}/cancel")
end
it 'returns the terminal_path of the build' do
expect(subject.terminal_path).to end_with("/jobs/#{build.id}/terminal.ws")
end
end
......@@ -16,4 +16,131 @@ describe Ci::BuildPolicy do
it_behaves_like 'protected environments access'
end
describe 'manage a web ide terminal' do
let(:build_permissions) { %i[read_web_ide_terminal create_build_terminal update_web_ide_terminal] }
set(:maintainer) { create(:user) }
let(:owner) { create(:owner) }
let(:admin) { create(:admin) }
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project, :public, namespace: owner.namespace) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, source: :webide) }
let(:build) { create(:ci_build, pipeline: pipeline) }
before do
stub_licensed_features(web_ide_terminal: true)
allow(build).to receive(:has_terminal?).and_return(true)
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest)
end
subject { described_class.new(current_user, build) }
context 'when create_web_ide_terminal access disabled' do
let(:current_user) { admin }
before do
stub_licensed_features(web_ide_terminal: false)
expect(current_user.can?(:create_web_ide_terminal, project)).to eq false
end
it { expect_disallowed(*build_permissions) }
end
context 'when create_web_ide_terminal access enabled' do
context 'with admin' do
let(:current_user) { admin }
it { expect_allowed(*build_permissions) }
context 'when build is not from a webide pipeline' do
let(:pipeline) { create(:ci_empty_pipeline, project: project, source: :chat) }
it { expect_disallowed(:read_web_ide_terminal, :update_web_ide_terminal) }
end
context 'when build has no runner terminal' do
before do
allow(build).to receive(:has_terminal?).and_return(false)
end
it { expect_allowed(:read_web_ide_terminal, :update_web_ide_terminal) }
it { expect_disallowed(:create_build_terminal) }
end
end
shared_examples 'allowed build owner access' do
it { expect_disallowed(*build_permissions) }
context 'when user is the owner of the job' do
let(:build) { create(:ci_build, pipeline: pipeline, user: current_user) }
it { expect_allowed(*build_permissions) }
end
end
shared_examples 'forbidden access' do
it { expect_disallowed(*build_permissions) }
context 'when user is the owner of the job' do
let(:build) { create(:ci_build, pipeline: pipeline, user: current_user) }
it { expect_disallowed(*build_permissions) }
end
end
context 'with owner' do
let(:current_user) { owner }
it_behaves_like 'allowed build owner access'
end
context 'with maintainer' do
let(:current_user) { maintainer }
it_behaves_like 'allowed build owner access'
end
context 'with developer' do
let(:current_user) { developer }
it_behaves_like 'forbidden access'
end
context 'with reporter' do
let(:current_user) { reporter }
it_behaves_like 'forbidden access'
end
context 'with guest' do
let(:current_user) { guest }
it_behaves_like 'forbidden access'
end
context 'with non member' do
let(:current_user) { create(:user) }
it_behaves_like 'forbidden access'
end
end
def expect_allowed(*permissions)
permissions.each { |p| is_expected.to be_allowed(p) }
end
def expect_disallowed(*permissions)
permissions.each do |p|
is_expected.not_to be_allowed(p)
end
end
end
end
......@@ -631,4 +631,70 @@ describe ProjectPolicy do
it { is_expected.to be_allowed(:read_software_license_policy) }
end
end
describe 'create_web_ide_terminal' do
before do
stub_licensed_features(web_ide_terminal: true)
end
subject { described_class.new(current_user, project) }
context 'without ide terminal feature available' do
before do
stub_licensed_features(web_ide_terminal: false)
end
let(:current_user) { admin }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:create_web_ide_terminal) }
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:create_web_ide_terminal) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_allowed(:create_web_ide_terminal) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
context 'with non member' do
let(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:create_web_ide_terminal) }
end
end
end
......@@ -14,12 +14,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
describe 'POST /api/v4/jobs/request' do
context 'for web-ide job' do
let(:user) { create(:user) }
let(:service) { Ci::CreateWebideTerminalService.new(project, user, ref: 'master').execute }
let(:service) { Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute }
let(:pipeline) { service[:pipeline] }
let(:build) { pipeline.builds.first }
before do
stub_licensed_features(webide_terminal: true)
stub_licensed_features(web_ide_terminal: true)
stub_webide_config_file(config_content)
project.add_maintainer(user)
......
# frozen_string_literal: true
require 'spec_helper'
describe WebIdeTerminalEntity do
let(:build) { create(:ci_build) }
let(:entity) { described_class.new(WebIdeTerminal.new(build)) }
subject { entity.as_json }
it { is_expected.to have_key(:id) }
it { is_expected.to have_key(:status) }
it { is_expected.to have_key(:show_path) }
it { is_expected.to have_key(:cancel_path) }
it { is_expected.to have_key(:retry_path) }
it { is_expected.to have_key(:terminal_path) }
end
# frozen_string_literal: true
require 'spec_helper'
describe WebIdeTerminalSerializer do
let(:build) { create(:ci_build) }
subject { described_class.new.represent(WebIdeTerminal.new(build)) }
it 'represents WebIdeTerminalEntity entities' do
expect(described_class.entity_class).to eq(WebIdeTerminalEntity)
end
it 'accepts WebIdeTerminal as a resource' do
expect(subject[:id]).to eq build.id
end
context 'when resource is a build' do
subject { described_class.new.represent(build) }
it 'transforms it into a WebIdeTerminal resource' do
expect(WebIdeTerminal).to receive(:new)
subject
end
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