Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d91ff791fb
commit
8e42824b11
|
|
@ -1,28 +1,14 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
import { AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY } from '../constants';
|
||||
|
||||
export const awsTokens = {
|
||||
[AWS_ACCESS_KEY_ID]: {
|
||||
name: AWS_ACCESS_KEY_ID,
|
||||
/* Checks for exactly twenty characters that match key.
|
||||
Based on greps suggested by Amazon at:
|
||||
https://aws.amazon.com/blogs/security/a-safer-way-to-distribute-aws-credentials-to-ec2/
|
||||
*/
|
||||
validation: val => /^[A-Za-z0-9]{20}$/.test(val),
|
||||
invalidMessage: __('This variable does not match the expected pattern.'),
|
||||
},
|
||||
[AWS_DEFAULT_REGION]: {
|
||||
name: AWS_DEFAULT_REGION,
|
||||
},
|
||||
[AWS_SECRET_ACCESS_KEY]: {
|
||||
name: AWS_SECRET_ACCESS_KEY,
|
||||
/* Checks for exactly forty characters that match secret.
|
||||
Based on greps suggested by Amazon at:
|
||||
https://aws.amazon.com/blogs/security/a-safer-way-to-distribute-aws-credentials-to-ec2/
|
||||
*/
|
||||
validation: val => /^[A-Za-z0-9/+=]{40}$/.test(val),
|
||||
invalidMessage: __('This variable does not match the expected pattern.'),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,14 +59,14 @@ export default {
|
|||
</div>
|
||||
<div class="col-4 col-md-3 gl-pl-0">
|
||||
<loading-button
|
||||
class="js-error-tracking-connect prepend-left-5 d-inline-flex"
|
||||
class="js-error-tracking-connect gl-ml-2 d-inline-flex"
|
||||
:label="isLoadingProjects ? __('Connecting') : __('Connect')"
|
||||
:loading="isLoadingProjects"
|
||||
@click="fetchProjects"
|
||||
/>
|
||||
<icon
|
||||
v-show="connectSuccessful"
|
||||
class="js-error-tracking-connect-success prepend-left-5 text-success align-middle"
|
||||
class="js-error-tracking-connect-success gl-ml-2 text-success align-middle"
|
||||
:aria-label="__('Projects Successfully Retrieved')"
|
||||
name="check-circle"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function membersBeforeSave(members) {
|
|||
const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
|
||||
const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
|
||||
const avatarIcon = member.mentionsDisabled
|
||||
? spriteIcon('notifications-off', 's16 vertical-align-middle prepend-left-5')
|
||||
? spriteIcon('notifications-off', 's16 vertical-align-middle gl-ml-2')
|
||||
: '';
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export default {
|
|||
data-container="body"
|
||||
data-placement="right"
|
||||
name="file-modified"
|
||||
class="prepend-left-5 ide-file-modified"
|
||||
class="gl-ml-2 ide-file-modified"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function mountIssuableListRootApp() {
|
|||
}
|
||||
|
||||
function mountIssuablesListApp() {
|
||||
if (!gon.features?.vueIssuablesList) {
|
||||
if (!gon.features?.vueIssuablesList && !gon.features?.jiraIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export default {
|
|||
<gl-link
|
||||
v-if="rawPath"
|
||||
:href="rawPath"
|
||||
class="js-raw-link text-plain text-underline prepend-left-5"
|
||||
class="js-raw-link text-plain text-underline gl-ml-2"
|
||||
>{{ s__('Job|Complete Raw') }}</gl-link
|
||||
>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ export default {
|
|||
<div>
|
||||
{{ headerText }}
|
||||
<slot :name="slotName"></slot>
|
||||
<popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
|
||||
<popover v-if="hasPopover" :options="popoverOptions" class="gl-ml-2" />
|
||||
</div>
|
||||
<slot name="subHeading"></slot>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ module DashboardHelper
|
|||
|
||||
if doc_href.present?
|
||||
link_to_doc = link_to(sprite_icon('question', size: 16), doc_href,
|
||||
class: 'prepend-left-5', title: _('Documentation'),
|
||||
class: 'gl-ml-2', title: _('Documentation'),
|
||||
target: '_blank', rel: 'noopener noreferrer')
|
||||
|
||||
concat(link_to_doc)
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class JiraService < IssueTrackerService
|
|||
end
|
||||
|
||||
def new_issue_url
|
||||
"#{url}/secure/CreateIssue.jspa"
|
||||
"#{url}/secure/CreateIssue!default.jspa"
|
||||
end
|
||||
|
||||
alias_method :original_url, :url
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :update_alert_management_alert
|
||||
enable :create_design
|
||||
enable :destroy_design
|
||||
enable :read_terraform_state
|
||||
end
|
||||
|
||||
rule { can?(:developer_access) & user_confirmed? }.policy do
|
||||
|
|
|
|||
|
|
@ -5,26 +5,17 @@ module Terraform
|
|||
include Gitlab::OptimisticLocking
|
||||
|
||||
StateLockedError = Class.new(StandardError)
|
||||
UnauthorizedError = Class.new(StandardError)
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_with_lock
|
||||
raise ArgumentError unless params[:name].present?
|
||||
|
||||
state = Terraform::State.find_by(project: project, name: params[:name])
|
||||
raise ActiveRecord::RecordNotFound.new("Couldn't find state") unless state
|
||||
|
||||
retry_optimistic_lock(state) { |state| yield state } if state && block_given?
|
||||
state
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def create_or_find!
|
||||
raise ArgumentError unless params[:name].present?
|
||||
|
||||
Terraform::State.create_or_find_by(project: project, name: params[:name])
|
||||
retrieve_with_lock(find_only: true) do |state|
|
||||
yield state if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
def handle_with_lock
|
||||
raise UnauthorizedError unless can_modify_state?
|
||||
|
||||
retrieve_with_lock do |state|
|
||||
raise StateLockedError unless lock_matches?(state)
|
||||
|
||||
|
|
@ -36,6 +27,7 @@ module Terraform
|
|||
|
||||
def lock!
|
||||
raise ArgumentError if params[:lock_id].blank?
|
||||
raise UnauthorizedError unless can_modify_state?
|
||||
|
||||
retrieve_with_lock do |state|
|
||||
raise StateLockedError if state.locked?
|
||||
|
|
@ -49,6 +41,8 @@ module Terraform
|
|||
end
|
||||
|
||||
def unlock!
|
||||
raise UnauthorizedError unless can_modify_state?
|
||||
|
||||
retrieve_with_lock do |state|
|
||||
# force-unlock does not pass ID, so we ignore it if it is missing
|
||||
raise StateLockedError unless params[:lock_id].nil? || lock_matches?(state)
|
||||
|
|
@ -63,8 +57,21 @@ module Terraform
|
|||
|
||||
private
|
||||
|
||||
def retrieve_with_lock
|
||||
create_or_find!.tap { |state| retry_optimistic_lock(state) { |state| yield state } }
|
||||
def retrieve_with_lock(find_only: false)
|
||||
create_or_find!(find_only: find_only).tap { |state| retry_optimistic_lock(state) { |state| yield state } }
|
||||
end
|
||||
|
||||
def create_or_find!(find_only:)
|
||||
raise ArgumentError unless params[:name].present?
|
||||
|
||||
find_params = { project: project, name: params[:name] }
|
||||
|
||||
if find_only
|
||||
Terraform::State.find_by(find_params) || # rubocop: disable CodeReuse/ActiveRecord
|
||||
raise(ActiveRecord::RecordNotFound.new("Couldn't find state"))
|
||||
else
|
||||
Terraform::State.create_or_find_by(find_params)
|
||||
end
|
||||
end
|
||||
|
||||
def lock_matches?(state)
|
||||
|
|
@ -73,5 +80,9 @@ module Terraform
|
|||
ActiveSupport::SecurityUtils
|
||||
.secure_compare(state.lock_xid.to_s, params[:lock_id].to_s)
|
||||
end
|
||||
|
||||
def can_modify_state?
|
||||
current_user.can?(:admin_terraform_state, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do
|
||||
= branch.name
|
||||
- if branch.name == @repository.root_ref
|
||||
%span.badge.badge-primary.prepend-left-5 default
|
||||
%span.badge.badge-primary.gl-ml-2 default
|
||||
- elsif merged
|
||||
%span.badge.badge-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
|
||||
%span.badge.badge-info.has-tooltip.gl-ml-2{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
|
||||
= s_('Branches|merged')
|
||||
|
||||
- if protected_branch?(@project, branch)
|
||||
%span.badge.badge-success.prepend-left-5
|
||||
%span.badge.badge-success.gl-ml-2
|
||||
= s_('Branches|protected')
|
||||
|
||||
= render_if_exists 'projects/branches/diverged_from_upstream', branch: branch
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: _('Project has too many %{label_for_message} to search') % { label_for_message: label_for_message } } }
|
||||
.has-tooltip{ class: "limit-box limit-box-#{objects} gl-ml-2", data: { title: _('Project has too many %{label_for_message} to search') % { label_for_message: label_for_message } } }
|
||||
.limit-icon
|
||||
- if objects == :branch
|
||||
= sprite_icon('fork', size: 12)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
Showing
|
||||
%button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }<
|
||||
= pluralize(diff_files.size, "changed file")
|
||||
= icon("caret-down", class: "prepend-left-5")
|
||||
= icon("caret-down", class: "gl-ml-2")
|
||||
%span.diff-stats-additions-deletions-expanded#diff-stats
|
||||
with
|
||||
%strong.cgreen= pluralize(sum_added_lines, 'addition')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
= link_to matching_branch.name, project_ref_path(@project, matching_branch.name), class: 'ref-name'
|
||||
|
||||
- if @project.root_ref?(matching_branch.name)
|
||||
%span.badge.badge-info.prepend-left-5 default
|
||||
%span.badge.badge-info.gl-ml-2 default
|
||||
%td
|
||||
- commit = @project.commit(matching_branch.name)
|
||||
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
= link_to matching_tag.name, project_ref_path(@project, matching_tag.name), class: 'ref-name'
|
||||
|
||||
- if @project.root_ref?(matching_tag.name)
|
||||
%span.badge.badge-info.prepend-left-5 default
|
||||
%span.badge.badge-info.gl-ml-2 default
|
||||
%td
|
||||
- commit = @project.commit(matching_tag.name)
|
||||
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
%span.ref-name= protected_tag.name
|
||||
|
||||
- if @project.root_ref?(protected_tag.name)
|
||||
%span.badge.badge-info.prepend-left-5 default
|
||||
%span.badge.badge-info.gl-ml-2 default
|
||||
%td
|
||||
- if protected_tag.wildcard?
|
||||
- matching_tags = protected_tag.matching(repository.tags)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
%span.cgray= starrer.user.to_reference
|
||||
|
||||
- if starrer.user == current_user
|
||||
%span.badge.badge-success.prepend-left-5= _("It's you")
|
||||
%span.badge.badge-success.gl-ml-2= _("It's you")
|
||||
|
||||
.block-truncated
|
||||
= time_ago_with_tooltip(starrer.starred_since)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
%a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name }
|
||||
%span= tree_row_name
|
||||
- if @lfs_blob_ids.include?(tree_row.id)
|
||||
%span.badge.label-lfs.prepend-left-5 LFS
|
||||
%span.badge.label-lfs.gl-ml-2 LFS
|
||||
|
||||
- elsif tree_row_type == :commit
|
||||
= tree_icon('archive', tree_row.mode, tree_row.name)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
= link_to namespace_project_issue_path(issue.project.namespace.becomes(Namespace), issue.project, issue) do
|
||||
%span.term.str-truncated= issue.title
|
||||
- if issue.closed?
|
||||
%span.badge.badge-danger.prepend-left-5= _("Closed")
|
||||
%span.badge.badge-danger.gl-ml-2= _("Closed")
|
||||
.float-right ##{issue.iid}
|
||||
- if issue.description.present?
|
||||
.description.term
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
= link_to namespace_project_merge_request_path(merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request) do
|
||||
%span.term.str-truncated= merge_request.title
|
||||
- if merge_request.merged?
|
||||
%span.badge.badge-primary.prepend-left-5= _("Merged")
|
||||
%span.badge.badge-primary.gl-ml-2= _("Merged")
|
||||
- elsif merge_request.closed?
|
||||
%span.badge.badge-danger.prepend-left-5= _("Closed")
|
||||
%span.badge.badge-danger.gl-ml-2= _("Closed")
|
||||
.float-right= merge_request.to_reference
|
||||
- if merge_request.description.present?
|
||||
.description.term
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
- elsif issuable.for_fork?
|
||||
%code= issuable.target_project_path + ":"
|
||||
- unless issuable.new_record?
|
||||
%span.dropdown.prepend-left-5.d-inline-block
|
||||
%span.dropdown.gl-ml-2.d-inline-block
|
||||
= form.hidden_field(:target_branch,
|
||||
{ class: 'target_branch js-target-branch-select ref-name mw-xl',
|
||||
data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }})
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
- if show_controls && member.source == current_resource
|
||||
|
||||
- if member.can_resend_invite?
|
||||
= link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]),
|
||||
= link_to sprite_icon('paper-airplane', size: 16), polymorphic_path([:resend_invite, member]),
|
||||
method: :post,
|
||||
class: 'btn btn-default align-self-center mr-sm-2',
|
||||
title: _('Resend invite')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow developer role read-only access to Terraform state
|
||||
merge_request: 33573
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove CI/CD variable validations on AWS keys
|
||||
merge_request: 36679
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Trigger stackprof by sending a SIGUSR2 signal
|
||||
merge_request: 35993
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# trigger stackprof by sending a SIGUSR2 signal
|
||||
#
|
||||
# default settings:
|
||||
# * collect raw samples
|
||||
# * sample at 100hz (every 10k microseconds)
|
||||
# * timeout profile after 30 seconds
|
||||
# * write to $TMPDIR/stackprof.$PID.$RAND.profile
|
||||
|
||||
if Gitlab::Utils.to_boolean(ENV['STACKPROF_ENABLED'].to_s)
|
||||
Gitlab::Cluster::LifecycleEvents.on_worker_start do
|
||||
require 'stackprof'
|
||||
require 'tmpdir'
|
||||
|
||||
Gitlab::AppJsonLogger.info "stackprof: listening on SIGUSR2 signal"
|
||||
|
||||
# create a pipe in order to propagate signal out of the signal handler
|
||||
# see also: https://cr.yp.to/docs/selfpipe.html
|
||||
read, write = IO.pipe
|
||||
|
||||
# create a separate thread that polls for signals on the pipe.
|
||||
#
|
||||
# this way we do not execute in signal handler context, which
|
||||
# lifts restrictions and also serializes the calls in a thread-safe
|
||||
# manner.
|
||||
#
|
||||
# it's very similar to a goroutine and channel design.
|
||||
#
|
||||
# another nice benefit of this method is that we can timeout the
|
||||
# IO.select call, allowing the profile to automatically stop after
|
||||
# a given interval (by default 30 seconds), avoiding unbounded memory
|
||||
# growth from a profile that was started and never stopped.
|
||||
t = Thread.new do
|
||||
timeout_s = ENV['STACKPROF_TIMEOUT_S']&.to_i || 30
|
||||
current_timeout_s = nil
|
||||
loop do
|
||||
got_value = IO.select([read], nil, nil, current_timeout_s)
|
||||
read.getbyte if got_value
|
||||
|
||||
if StackProf.running?
|
||||
stackprof_file_prefix = ENV['STACKPROF_FILE_PREFIX'] || Dir.tmpdir
|
||||
stackprof_out_file = "#{stackprof_file_prefix}/stackprof.#{Process.pid}.#{SecureRandom.hex(6)}.profile"
|
||||
|
||||
Gitlab::AppJsonLogger.info(
|
||||
event: "stackprof",
|
||||
message: "stopping profile",
|
||||
output_filename: stackprof_out_file,
|
||||
pid: Process.pid,
|
||||
timeout_s: timeout_s,
|
||||
timed_out: got_value.nil?
|
||||
)
|
||||
|
||||
StackProf.stop
|
||||
StackProf.results(stackprof_out_file)
|
||||
current_timeout_s = nil
|
||||
else
|
||||
Gitlab::AppJsonLogger.info(
|
||||
event: "stackprof",
|
||||
message: "starting profile",
|
||||
pid: Process.pid
|
||||
)
|
||||
|
||||
StackProf.start(
|
||||
raw: Gitlab::Utils.to_boolean(ENV['STACKPROF_RAW'] || 'true'),
|
||||
interval: ENV['STACKPROF_INTERVAL_US']&.to_i || 10_000
|
||||
)
|
||||
current_timeout_s = timeout_s
|
||||
end
|
||||
end
|
||||
end
|
||||
t.abort_on_exception = true
|
||||
|
||||
# in the case of puma, this will override the existing SIGUSR2 signal handler
|
||||
# that can be used to trigger a restart.
|
||||
#
|
||||
# puma cluster has two types of restarts:
|
||||
# * SIGUSR1: phased restart
|
||||
# * SIGUSR2: restart
|
||||
#
|
||||
# phased restart is not supported in our configuration, because we use
|
||||
# preload_app. this means we will always perform a normal restart.
|
||||
# additionally, phased restart is not supported when sending a SIGUSR2
|
||||
# directly to a puma worker (as opposed to the master process).
|
||||
#
|
||||
# the result is that the behaviour of SIGUSR1 and SIGUSR2 is identical in
|
||||
# our configuration, and we can always use a SIGUSR1 to perform a restart.
|
||||
#
|
||||
# thus, it is acceptable for us to re-appropriate the SIGUSR2 signal, and
|
||||
# override the puma behaviour.
|
||||
#
|
||||
# see also:
|
||||
# * https://github.com/puma/puma/blob/master/docs/signals.md#puma-signals
|
||||
# * https://github.com/phusion/unicorn/blob/master/SIGNALS
|
||||
# * https://github.com/mperham/sidekiq/wiki/Signals
|
||||
Signal.trap('SIGUSR2') do
|
||||
write.write('.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Gitlab::Seeder::Packages
|
||||
attr_reader :project
|
||||
|
||||
def initialize(project)
|
||||
@project = project
|
||||
end
|
||||
|
||||
def seed_packages(package_type)
|
||||
send("seed_#{package_type}_packages")
|
||||
end
|
||||
|
||||
def seed_npm_packages
|
||||
5.times do |i|
|
||||
name = "@#{@project.root_namespace.path}/npm_package_#{SecureRandom.hex}"
|
||||
version = "1.12.#{i}"
|
||||
|
||||
params = Gitlab::Json.parse(read_fixture_file('npm', 'payload.json')
|
||||
.gsub('@root/npm-test', name)
|
||||
.gsub('1.0.1', version))
|
||||
.with_indifferent_access
|
||||
|
||||
::Packages::Npm::CreatePackageService.new(project, project.owner, params).execute
|
||||
|
||||
print '.'
|
||||
end
|
||||
end
|
||||
|
||||
def seed_maven_packages
|
||||
5.times do |i|
|
||||
name = "my/company/app/maven-app-#{i}"
|
||||
version = "1.0.#{i}-SNAPSHOT"
|
||||
|
||||
params = {
|
||||
name: name,
|
||||
version: version,
|
||||
path: "#{name}/#{version}"
|
||||
}
|
||||
|
||||
pkg = ::Packages::Maven::CreatePackageService.new(project, project.owner, params).execute
|
||||
|
||||
%w(maven-metadata.xml my-app-1.0-20180724.124855-1.pom my-app-1.0-20180724.124855-1.jar).each do |filename|
|
||||
with_cloned_fixture_file('maven', filename) do |filepath|
|
||||
file_params = {
|
||||
file: UploadedFile.new(filepath, filename: filename),
|
||||
file_name: filename,
|
||||
file_sha1: '1234567890',
|
||||
size: 100.kilobytes
|
||||
}
|
||||
::Packages::CreatePackageFileService.new(pkg, file_params).execute
|
||||
end
|
||||
end
|
||||
|
||||
print '.'
|
||||
end
|
||||
end
|
||||
|
||||
def seed_conan_packages
|
||||
5.times do |i|
|
||||
name = "my-conan-pkg-#{i}"
|
||||
version = "2.0.#{i}"
|
||||
|
||||
params = {
|
||||
package_name: name,
|
||||
package_version: version,
|
||||
package_username: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path),
|
||||
package_channel: 'stable'
|
||||
}
|
||||
|
||||
pkg = ::Packages::Conan::CreatePackageService.new(project, project.owner, params).execute
|
||||
|
||||
fixtures = {
|
||||
'recipe_files' => %w(conanfile.py conanmanifest.txt),
|
||||
'package_files' => %w(conanmanifest.txt conaninfo.txt conan_package.tgz)
|
||||
}
|
||||
|
||||
fixtures.each do |folder, filenames|
|
||||
filenames.each do |filename|
|
||||
with_cloned_fixture_file(File.join('conan', folder), filename) do |filepath|
|
||||
file = UploadedFile.new(filepath, filename: filename)
|
||||
file_params = {
|
||||
file_name: filename,
|
||||
'file.sha1': '1234567890',
|
||||
'file.size': 100.kilobytes,
|
||||
'file.md5': '12345',
|
||||
recipe_revision: '0',
|
||||
package_revision: '0',
|
||||
conan_package_reference: '123456789',
|
||||
conan_file_type: :package_file
|
||||
}
|
||||
::Packages::Conan::CreatePackageFileService.new(pkg, file, file_params).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print '.'
|
||||
end
|
||||
end
|
||||
|
||||
def seed_nuget_packages
|
||||
5.times do |i|
|
||||
name = "MyNugetApp.Package#{i}"
|
||||
version = "4.2.#{i}"
|
||||
|
||||
pkg = ::Packages::Nuget::CreatePackageService.new(project, project.owner, {}).execute
|
||||
# when using ::Packages::Nuget::CreatePackageService, packages have a fixed name and a fixed version.
|
||||
pkg.update!(name: name, version: version)
|
||||
|
||||
filename = 'package.nupkg'
|
||||
with_cloned_fixture_file('nuget', filename) do |filepath|
|
||||
file_params = {
|
||||
file: UploadedFile.new(filepath, filename: filename),
|
||||
file_name: filename,
|
||||
file_sha1: '1234567890',
|
||||
size: 100.kilobytes
|
||||
}
|
||||
::Packages::CreatePackageFileService.new(pkg, file_params).execute
|
||||
end
|
||||
|
||||
print '.'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_fixture_file(package_type, file)
|
||||
File.read(fixture_path(package_type, file))
|
||||
end
|
||||
|
||||
def fixture_path(package_type, file)
|
||||
Rails.root.join('spec', 'fixtures', 'packages', package_type, file)
|
||||
end
|
||||
|
||||
def with_cloned_fixture_file(package_type, file)
|
||||
Dir.mktmpdir do |dirpath|
|
||||
cloned_path = File.join(dirpath, file)
|
||||
FileUtils.cp(fixture_path(package_type, file), cloned_path)
|
||||
yield cloned_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Seeder.quiet do
|
||||
flag = 'SEED_ALL_PACKAGE_TYPES'
|
||||
|
||||
puts "Use the `#{flag}` environment variable to seed packages of all types." unless ENV[flag]
|
||||
|
||||
package_types = ENV[flag] ? %i[npm maven conan nuget] : [:npm]
|
||||
|
||||
Project.not_mass_generated.sample(5).each do |project|
|
||||
puts "\nSeeding packages for the '#{project.full_path}' project"
|
||||
seeder = Gitlab::Seeder::Packages.new(project)
|
||||
|
||||
package_types.each do |package_type|
|
||||
seeder.seed_packages(package_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -233,13 +233,12 @@ be updated or viewed by project members with [maintainer permissions](../../user
|
|||
### Custom variables validated by GitLab
|
||||
|
||||
Some variables are listed in the UI so you can choose them more quickly.
|
||||
GitLab validates the values of these variables to ensure they are in the correct format.
|
||||
|
||||
| Variable | Allowed Values | Introduced in |
|
||||
|-------------------------|----------------------------------------------------|---------------|
|
||||
| `AWS_ACCESS_KEY_ID` | 20 characters: letters, digits | 12.10 |
|
||||
| `AWS_ACCESS_KEY_ID` | Any | 12.10 |
|
||||
| `AWS_DEFAULT_REGION` | Any | 12.10 |
|
||||
| `AWS_SECRET_ACCESS_KEY` | 40 characters: letters, digits, special characters | 12.10 |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Any | 12.10 |
|
||||
|
||||
NOTE: **Note:**
|
||||
When you store credentials, there are security implications. If you are using AWS keys,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ graphs/dashboards.
|
|||
GitLab provides built-in tools to help improve performance and availability:
|
||||
|
||||
- [Profiling](profiling.md).
|
||||
- [Sherlock](profiling.md#sherlock).
|
||||
- [Distributed Tracing](distributed_tracing.md)
|
||||
- [GitLab Performance Monitoring](../administration/monitoring/performance/index.md).
|
||||
- [Request Profiling](../administration/monitoring/performance/request_profiling.md).
|
||||
|
|
@ -108,16 +107,24 @@ In short:
|
|||
## Profiling
|
||||
|
||||
By collecting snapshots of process state at regular intervals, profiling allows
|
||||
you to see where time is spent in a process. The [StackProf](https://github.com/tmm1/stackprof)
|
||||
gem is included in GitLab's development environment, allowing you to investigate
|
||||
the behavior of suspect code in detail.
|
||||
you to see where time is spent in a process. The
|
||||
[Stackprof](https://github.com/tmm1/stackprof) gem is included in GitLab,
|
||||
allowing you to profile which code is running on CPU in detail.
|
||||
|
||||
It's important to note that profiling an application *alters its performance*,
|
||||
and will generally be done *in an unrepresentative environment*. In particular,
|
||||
a method is not necessarily troublesome just because it's executed many times,
|
||||
or takes a long time to execute. Profiles are tools you can use to better
|
||||
understand what is happening in an application - using that information wisely
|
||||
is up to you!
|
||||
It's important to note that profiling an application *alters its performance*.
|
||||
Different profiling strategies have different overheads. Stackprof is a sampling
|
||||
profiler. It will sample stack traces from running threads at a configurable
|
||||
frequency (e.g. 100hz, that is 100 stacks per second). This type of profiling
|
||||
has quite a low (albeit non-zero) overhead and is generally considered to be
|
||||
safe for production.
|
||||
|
||||
### Development
|
||||
|
||||
A profiler can be a very useful tool during development, even if it does run *in
|
||||
an unrepresentative environment*. In particular, a method is not necessarily
|
||||
troublesome just because it's executed many times, or takes a long time to
|
||||
execute. Profiles are tools you can use to better understand what is happening
|
||||
in an application - using that information wisely is up to you!
|
||||
|
||||
Keeping that in mind, to create a profile, identify (or create) a spec that
|
||||
exercises the troublesome code path, then run it using the `bin/rspec-stackprof`
|
||||
|
|
@ -211,11 +218,56 @@ application code, these profiles can be used to investigate slow tests as well.
|
|||
However, for smaller runs (like this example), this means that the cost of
|
||||
setting up the test suite will tend to dominate.
|
||||
|
||||
It's also possible to modify the application code in-place to output profiles
|
||||
whenever a particular code path is triggered without going through the test
|
||||
suite first. See the
|
||||
[StackProf documentation](https://github.com/tmm1/stackprof/blob/master/README.md)
|
||||
for details.
|
||||
### Production
|
||||
|
||||
Stackprof can also be used to profile production workloads.
|
||||
|
||||
In order to enable production profiling for Ruby processes, you can set the `STACKPROF_ENABLED` environment variable to `true`.
|
||||
|
||||
The following configuration options can be configured:
|
||||
|
||||
- `STACKPROF_ENABLED`: Enables stackprof signal handler on SIGUSR2 signal.
|
||||
Defaults to `false`.
|
||||
- `STACKPROF_INTERVAL_US`: Sampling interval in microseconds. Defaults to
|
||||
`10000` μs (100hz).
|
||||
- `STACKPROF_FILE_PREFIX`: File path prefix where profiles are stored. Defaults
|
||||
to `$TMPDIR` (often corresponds to `/tmp`).
|
||||
- `STACKPROF_TIMEOUT_S`: Profiling timeout in seconds. Profiling will
|
||||
automatically stop after this time has elapsed. Defaults to `30`.
|
||||
- `STACKPROF_RAW`: Whether to collect raw samples or only aggregates. Raw
|
||||
samples are needed to generate flamegraphs, but they do have a higher memory
|
||||
and disk overhead. Defaults to `true`.
|
||||
|
||||
Once enabled, profiling can be triggered by sending a `SIGUSR2` signal to the
|
||||
Ruby process. The process will begin sampling stacks. Profiling can be stopped
|
||||
by sending another `SIGUSR2`. Alternatively, it will automatically stop after
|
||||
the timeout.
|
||||
|
||||
Once profiling stops, the profile is written out to disk at
|
||||
`$STACKPROF_FILE_PREFIX/stackprof.$PID.$RAND.profile`. It can then be inspected
|
||||
further via the `stackprof` command line tool, as described in the previous
|
||||
section.
|
||||
|
||||
Currently supported profiling targets are:
|
||||
|
||||
- Puma worker
|
||||
- Sidekiq
|
||||
|
||||
NOTE: **Note:** The Puma master process is not supported. Neither is Unicorn.
|
||||
Sending SIGUSR2 to either of those will trigger restarts. In the case of Puma,
|
||||
take care to only send the signal to Puma workers.
|
||||
|
||||
This can be done via `pkill -USR2 puma:`. The `:` disambiguates between `puma
|
||||
4.3.3.gitlab.2 ...` (the master process) from `puma: cluster worker 0: ...` (the
|
||||
worker processes), selecting the latter.
|
||||
|
||||
Production profiles can be especially noisy. It can be helpful to visualize them
|
||||
as a [flamegraph](https://github.com/brendangregg/FlameGraph). This can be done
|
||||
via:
|
||||
|
||||
```shell
|
||||
bundle exec stackprof --stackcollapse /tmp/stackprof.55769.c6c3906452.profile | flamegraph.pl > flamegraph.svg
|
||||
```
|
||||
|
||||
## RSpec profiling
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Telemetry Guide:
|
|||
1. [Our tracking tools](#our-tracking-tools)
|
||||
1. [What data can be tracked](#what-data-can-be-tracked)
|
||||
1. [Telemetry systems overview](#telemetry-systems-overview)
|
||||
1. [Snowflake data warehouse](#snowflake-data-warehouse)
|
||||
|
||||
[Usage Ping Guide](usage_ping.md)
|
||||
|
||||
|
|
@ -169,3 +170,19 @@ The differences between GitLab.com and self-managed are summarized below:
|
|||
| Self-Managed | **{dotted-circle}**(1) | **{dotted-circle}**(1) | **{check-circle}** | **{dotted-circle}** | **{dotted-circle}** |
|
||||
|
||||
Note (1): Snowplow JS and Snowplow Ruby are available on self-managed, however, the Snowplow Collector endpoint is set to a self-managed Snowplow Collector which GitLab Inc does not have access to.
|
||||
|
||||
## Snowflake data warehouse
|
||||
|
||||
The Snowflake data warehouse is where we keep all of GitLab Inc's data.
|
||||
|
||||
### Data sources
|
||||
|
||||
There are several data sources available in Snowflake and Sisense each representing a different view of the data along the transformation pipeline.
|
||||
|
||||
| Source | Description | Access |
|
||||
| ------ | ------ | ------ |
|
||||
| raw | These tables are the raw data source | Access via Snowflake |
|
||||
| analytics_staging | These tables have undergone little to no data transformation, meaning they're basically clones of the raw data source | Access via Snowflake or Sisense |
|
||||
| analytics | These tables have typically undergone more data transformation. They will typically end in `_xf` to represent the fact that they are transformed | Access via Snowflake or Sisense |
|
||||
|
||||
If you are a Product Manager interested in the raw data, you will likely focus on the `analytics` and `analytics_staging` sources. The raw source is limited to the data and infrastructure teams. For more information, please see [Data For Product Managers: What's the difference between analytics_staging and analytics?](https://about.gitlab.com/handbook/business-ops/data-team/programs/data-for-product-managers/#whats-the-difference-between-analytics_staging-and-analytics)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ To get started with a GitLab-managed Terraform State, there are two different op
|
|||
- [Use a local machine](#get-started-using-local-development).
|
||||
- [Use GitLab CI](#get-started-using-gitlab-ci).
|
||||
|
||||
## Permissions for using Terraform
|
||||
|
||||
In GitLab version 13.1, [Maintainer access](../permissions.md) was required to use a
|
||||
GitLab managed Terraform state backend. In GitLab versions 13.2 and greater,
|
||||
[Maintainer access](../permissions.md) is required to lock, unlock and write to the state
|
||||
(using `terraform apply`), while [Developer access](../permissions.md) is required to read
|
||||
the state (using `terraform plan -lock=false`).
|
||||
|
||||
## Get started using local development
|
||||
|
||||
If you plan to only run `terraform plan` and `terraform apply` commands from your
|
||||
|
|
@ -54,8 +62,7 @@ local machine, this is a simple way to get started:
|
|||
```
|
||||
|
||||
1. Create a [Personal Access Token](../profile/personal_access_tokens.md) with
|
||||
the `api` scope. The Terraform backend is restricted to users with
|
||||
[Maintainer access](../permissions.md) to the repository.
|
||||
the `api` scope.
|
||||
|
||||
1. On your local machine, run `terraform init`, passing in the following options,
|
||||
replacing `<YOUR-PROJECT-NAME>`, `<YOUR-PROJECT-ID>`, `<YOUR-USERNAME>` and
|
||||
|
|
@ -89,10 +96,6 @@ Next, [configure the backend](#configure-the-backend).
|
|||
After executing the `terraform init` command, you must configure the Terraform backend
|
||||
and the CI YAML file:
|
||||
|
||||
CAUTION: **Important:**
|
||||
The Terraform backend is restricted to users with [Maintainer access](../permissions.md)
|
||||
to the repository.
|
||||
|
||||
1. In your Terraform project, define the [HTTP backend](https://www.terraform.io/docs/backends/types/http.html)
|
||||
by adding the following code block in a `.tf` file (such as `backend.tf`) to
|
||||
define the remote backend:
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ The following table depicts the various user permission levels in a project.
|
|||
| Manage clusters | | | | ✓ | ✓ |
|
||||
| Manage Project Operations | | | | ✓ | ✓ |
|
||||
| View Pods logs | | | | ✓ | ✓ |
|
||||
| Read Terraform state | | | ✓ | ✓ | ✓ |
|
||||
| Manage Terraform state | | | | ✓ | ✓ |
|
||||
| Manage license policy **(ULTIMATE)** | | | | ✓ | ✓ |
|
||||
| Edit comments (posted by any user) | | | | ✓ | ✓ |
|
||||
| Manage Error Tracking | | | | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module API
|
|||
|
||||
before do
|
||||
authenticate!
|
||||
authorize! :admin_terraform_state, user_project
|
||||
authorize! :read_terraform_state, user_project
|
||||
end
|
||||
|
||||
params do
|
||||
|
|
@ -46,6 +46,8 @@ module API
|
|||
desc 'Add a new terraform state or update an existing one'
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
post do
|
||||
authorize! :admin_terraform_state, user_project
|
||||
|
||||
data = request.body.read
|
||||
no_content! if data.empty?
|
||||
|
||||
|
|
@ -59,6 +61,8 @@ module API
|
|||
desc 'Delete a terraform state of a certain name'
|
||||
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
delete do
|
||||
authorize! :admin_terraform_state, user_project
|
||||
|
||||
remote_state_handler.handle_with_lock do |state|
|
||||
state.destroy!
|
||||
status :ok
|
||||
|
|
@ -77,6 +81,8 @@ module API
|
|||
requires :Path, type: String, desc: 'Terraform path'
|
||||
end
|
||||
post '/lock' do
|
||||
authorize! :admin_terraform_state, user_project
|
||||
|
||||
status_code = :ok
|
||||
lock_info = {
|
||||
'Operation' => params[:Operation],
|
||||
|
|
@ -108,6 +114,8 @@ module API
|
|||
optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
|
||||
end
|
||||
delete '/lock' do
|
||||
authorize! :admin_terraform_state, user_project
|
||||
|
||||
remote_state_handler.unlock!
|
||||
status :ok
|
||||
rescue ::Terraform::RemoteStateHandler::StateLockedError
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
require 'logger'
|
||||
|
||||
desc "GitLab | Packages | Migrate packages files to remote storage"
|
||||
namespace :gitlab do
|
||||
namespace :packages do
|
||||
task migrate: :environment do
|
||||
logger = Logger.new(STDOUT)
|
||||
logger.info('Starting transfer of package files to object storage')
|
||||
|
||||
unless ::Packages::PackageFileUploader.object_store_enabled?
|
||||
raise 'Object store is disabled for packages feature'
|
||||
end
|
||||
|
||||
::Packages::PackageFile.with_files_stored_locally.find_each(batch_size: 10) do |package_file|
|
||||
package_file.file.migrate!(::Packages::PackageFileUploader::Store::REMOTE)
|
||||
|
||||
logger.info("Transferred package file #{package_file.id} of size #{package_file.size.to_i.bytes} to object storage")
|
||||
rescue => e
|
||||
logger.error("Failed to transfer package file #{package_file.id} with error: #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6871,6 +6871,9 @@ msgstr ""
|
|||
msgid "Create new file or directory"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create new issue in Jira"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create new label"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24066,9 +24069,6 @@ msgstr ""
|
|||
msgid "This variable can not be masked."
|
||||
msgstr ""
|
||||
|
||||
msgid "This variable does not match the expected pattern."
|
||||
msgstr ""
|
||||
|
||||
msgid "This will help us personalize your onboarding experience."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,11 @@ FactoryBot.define do
|
|||
trait :with_file do
|
||||
file { fixture_file_upload('spec/fixtures/terraform/terraform.tfstate', 'application/json') }
|
||||
end
|
||||
|
||||
trait :locked do
|
||||
sequence(:lock_xid) { |n| "lock-#{n}" }
|
||||
locked_at { Time.current }
|
||||
locked_by_user { create(:user) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { GlDeprecatedButton } from '@gitlab/ui';
|
|||
import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
|
||||
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
|
||||
import CiKeyField from '~/ci_variable_list/components/ci_key_field.vue';
|
||||
import { awsTokens } from '~/ci_variable_list/components/ci_variable_autocomplete_tokens';
|
||||
import createStore from '~/ci_variable_list/store';
|
||||
import mockData from '../services/mock_data';
|
||||
import ModalStub from '../stubs';
|
||||
|
|
@ -176,29 +175,6 @@ describe('Ci variable modal', () => {
|
|||
describe('Validations', () => {
|
||||
const maskError = 'This variable can not be masked.';
|
||||
|
||||
describe('when the key state is invalid', () => {
|
||||
beforeEach(() => {
|
||||
const [variable] = mockData.mockVariables;
|
||||
const invalidKeyVariable = {
|
||||
...variable,
|
||||
key: AWS_ACCESS_KEY_ID,
|
||||
value: 'AKIAIOSFODNN7EXAMPLEjdhy',
|
||||
secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy',
|
||||
};
|
||||
createComponent(mount);
|
||||
store.state.variable = invalidKeyVariable;
|
||||
});
|
||||
|
||||
it('disables the submit button', () => {
|
||||
expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the correct error text', () => {
|
||||
const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage;
|
||||
expect(findModal().text()).toContain(errorText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the mask state is invalid', () => {
|
||||
beforeEach(() => {
|
||||
const [variable] = mockData.mockVariables;
|
||||
|
|
@ -222,39 +198,14 @@ describe('Ci variable modal', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the mask and key states are invalid', () => {
|
||||
beforeEach(() => {
|
||||
const [variable] = mockData.mockVariables;
|
||||
const invalidMaskandKeyVariable = {
|
||||
...variable,
|
||||
key: AWS_ACCESS_KEY_ID,
|
||||
value: 'AKIAIOSFODNN7EXAMPLEjdhyd:;',
|
||||
secret_value: 'AKIAIOSFODNN7EXAMPLEjdhyd:;',
|
||||
masked: true,
|
||||
};
|
||||
createComponent(mount);
|
||||
store.state.variable = invalidMaskandKeyVariable;
|
||||
});
|
||||
|
||||
it('disables the submit button', () => {
|
||||
expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the correct error text', () => {
|
||||
const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage;
|
||||
expect(findModal().text()).toContain(maskError);
|
||||
expect(findModal().text()).toContain(errorText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when both states are valid', () => {
|
||||
beforeEach(() => {
|
||||
const [variable] = mockData.mockVariables;
|
||||
const validMaskandKeyVariable = {
|
||||
...variable,
|
||||
key: AWS_ACCESS_KEY_ID,
|
||||
value: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secret_value: 'AKIAIOSFODNN7EXAMPLE',
|
||||
value: '12345678',
|
||||
secret_value: '87654321',
|
||||
masked: true,
|
||||
};
|
||||
createComponent(mount);
|
||||
|
|
@ -265,12 +216,6 @@ describe('Ci variable modal', () => {
|
|||
it('does not disable the submit button', () => {
|
||||
expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows no error text', () => {
|
||||
const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage;
|
||||
expect(findModal().text()).not.toContain(maskError);
|
||||
expect(findModal().text()).not.toContain(errorText);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ describe('GfmAutoComplete', () => {
|
|||
title: 'My Group',
|
||||
search: 'my-group My Group',
|
||||
icon:
|
||||
'<svg class="s16 vertical-align-middle prepend-left-5"><use xlink:href="undefined#notifications-off" /></svg>',
|
||||
'<svg class="s16 vertical-align-middle gl-ml-2"><use xlink:href="undefined#notifications-off" /></svg>',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ RSpec.describe JiraService do
|
|||
|
||||
describe '#new_issue_url' do
|
||||
it 'handles trailing slashes' do
|
||||
expect(service.new_issue_url).to eq('http://jira.test.com/path/secure/CreateIssue.jspa')
|
||||
expect(service.new_issue_url).to eq('http://jira.test.com/path/secure/CreateIssue!default.jspa')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ RSpec.describe ProjectPolicy do
|
|||
resolve_note create_container_image update_container_image destroy_container_image daily_statistics
|
||||
create_environment update_environment create_deployment update_deployment create_release update_release
|
||||
create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation
|
||||
read_terraform_state
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -59,10 +59,11 @@ RSpec.describe API::Terraform::State do
|
|||
context 'with developer permissions' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it 'returns forbidden if the user cannot access the state' do
|
||||
it 'returns terraform state belonging to a project of given state name' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to eq(state.file.read)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -94,10 +95,11 @@ RSpec.describe API::Terraform::State do
|
|||
context 'with developer permissions' do
|
||||
let(:job) { create(:ci_build, project: project, user: developer) }
|
||||
|
||||
it 'returns forbidden if the user cannot access the state' do
|
||||
it 'returns terraform state belonging to a project of given state name' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to eq(state.file.read)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -235,9 +237,43 @@ RSpec.describe API::Terraform::State do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
context 'state is already locked' do
|
||||
before do
|
||||
state.update!(lock_xid: 'locked', locked_by_user: current_user)
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:conflict)
|
||||
end
|
||||
end
|
||||
|
||||
context 'user does not have permission to lock the state' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /projects/:id/terraform/state/:name/lock' do
|
||||
let(:params) do
|
||||
{
|
||||
ID: lock_id,
|
||||
Version: '0.1',
|
||||
Operation: 'OperationTypePlan',
|
||||
Info: '',
|
||||
Who: "#{current_user.username}",
|
||||
Created: Time.now.utc.iso8601(6),
|
||||
Path: ''
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
state.lock_xid = '123-456'
|
||||
state.save!
|
||||
|
|
@ -246,7 +282,7 @@ RSpec.describe API::Terraform::State do
|
|||
subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params }
|
||||
|
||||
context 'with the correct lock id' do
|
||||
let(:params) { { ID: '123-456' } }
|
||||
let(:lock_id) { '123-456' }
|
||||
|
||||
it 'removes the terraform state lock' do
|
||||
request
|
||||
|
|
@ -266,7 +302,7 @@ RSpec.describe API::Terraform::State do
|
|||
end
|
||||
|
||||
context 'with an incorrect lock id' do
|
||||
let(:params) { { ID: '456-789' } }
|
||||
let(:lock_id) { '456-789' }
|
||||
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
|
@ -276,7 +312,7 @@ RSpec.describe API::Terraform::State do
|
|||
end
|
||||
|
||||
context 'with a longer than 255 character lock id' do
|
||||
let(:params) { { ID: '0' * 256 } }
|
||||
let(:lock_id) { '0' * 256 }
|
||||
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
|
@ -284,5 +320,16 @@ RSpec.describe API::Terraform::State do
|
|||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'user does not have permission to unlock the state' do
|
||||
let(:lock_id) { '123-456' }
|
||||
let(:current_user) { developer }
|
||||
|
||||
it 'returns an error' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Terraform::RemoteStateHandler do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:developer) { create(:user, developer_projects: [project]) }
|
||||
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
|
||||
|
||||
let_it_be(:user) { maintainer }
|
||||
|
||||
describe '#find_with_lock' do
|
||||
context 'without a state name' do
|
||||
|
|
@ -34,33 +37,6 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#create_or_find!' do
|
||||
it 'requires passing a state name' do
|
||||
handler = described_class.new(project, user)
|
||||
|
||||
expect { handler.create_or_find! }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'allows to create states with same name in different projects' do
|
||||
project_b = create(:project)
|
||||
|
||||
state_a = described_class.new(project, user, name: 'my-state').create_or_find!
|
||||
state_b = described_class.new(project_b, user, name: 'my-state').create_or_find!
|
||||
|
||||
expect(state_a).to be_persisted
|
||||
expect(state_b).to be_persisted
|
||||
expect(state_a.id).not_to eq state_b.id
|
||||
end
|
||||
|
||||
it 'loads the same state upon subsequent call in the project scope' do
|
||||
state_a = described_class.new(project, user, name: 'my-state').create_or_find!
|
||||
state_b = described_class.new(project, user, name: 'my-state').create_or_find!
|
||||
|
||||
expect(state_a).to be_persisted
|
||||
expect(state_a.id).to eq state_b.id
|
||||
end
|
||||
end
|
||||
|
||||
context 'when state locking is not being used' do
|
||||
subject { described_class.new(project, user, name: 'my-state') }
|
||||
|
||||
|
|
@ -74,7 +50,7 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
end
|
||||
|
||||
it 'returns the state object itself' do
|
||||
state = subject.create_or_find!
|
||||
state = subject.handle_with_lock
|
||||
|
||||
expect(state.name).to eq 'my-state'
|
||||
end
|
||||
|
|
@ -89,10 +65,9 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
|
||||
context 'when using locking' do
|
||||
describe '#handle_with_lock' do
|
||||
it 'handles a locked state using exclusive read lock' do
|
||||
handler = described_class
|
||||
.new(project, user, name: 'new-state', lock_id: 'abc-abc')
|
||||
subject(:handler) { described_class.new(project, user, name: 'new-state', lock_id: 'abc-abc') }
|
||||
|
||||
it 'handles a locked state using exclusive read lock' do
|
||||
handler.lock!
|
||||
|
||||
state = handler.handle_with_lock do |state|
|
||||
|
|
@ -101,20 +76,35 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
|
||||
expect(state.name).to eq 'new-name'
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises exception if lock has not been acquired before' do
|
||||
handler = described_class
|
||||
.new(project, user, name: 'new-state', lock_id: 'abc-abc')
|
||||
it 'raises exception if lock has not been acquired before' do
|
||||
expect { handler.handle_with_lock }
|
||||
.to raise_error(described_class::StateLockedError)
|
||||
end
|
||||
|
||||
expect { handler.handle_with_lock }
|
||||
.to raise_error(described_class::StateLockedError)
|
||||
context 'user does not have permission to modify state' do
|
||||
let(:user) { developer }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { handler.handle_with_lock }
|
||||
.to raise_error(described_class::UnauthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#lock!' do
|
||||
it 'allows to lock state if it does not exist yet' do
|
||||
handler = described_class.new(project, user, name: 'new-state', lock_id: 'abc-abc')
|
||||
let(:lock_id) { 'abc-abc' }
|
||||
|
||||
subject(:handler) do
|
||||
described_class.new(
|
||||
project,
|
||||
user,
|
||||
name: 'new-state',
|
||||
lock_id: lock_id
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows to lock state if it does not exist yet' do
|
||||
state = handler.lock!
|
||||
|
||||
expect(state).to be_persisted
|
||||
|
|
@ -122,22 +112,61 @@ RSpec.describe Terraform::RemoteStateHandler do
|
|||
end
|
||||
|
||||
it 'allows to lock state if it exists and is not locked' do
|
||||
state = described_class.new(project, user, name: 'new-state').create_or_find!
|
||||
handler = described_class.new(project, user, name: 'new-state', lock_id: 'abc-abc')
|
||||
state = create(:terraform_state, project: project, name: 'new-state')
|
||||
|
||||
handler.lock!
|
||||
|
||||
expect(state.reload.lock_xid).to eq 'abc-abc'
|
||||
expect(state.reload.lock_xid).to eq lock_id
|
||||
expect(state).to be_locked
|
||||
end
|
||||
|
||||
it 'raises an exception when trying to unlocked state locked by someone else' do
|
||||
described_class.new(project, user, name: 'new-state', lock_id: 'abc-abc').lock!
|
||||
|
||||
handler = described_class.new(project, user, name: 'new-state', lock_id: '12a-23f')
|
||||
described_class.new(project, user, name: 'new-state', lock_id: '12a-23f').lock!
|
||||
|
||||
expect { handler.lock! }.to raise_error(described_class::StateLockedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unlock!' do
|
||||
let(:lock_id) { 'abc-abc' }
|
||||
|
||||
subject(:handler) do
|
||||
described_class.new(
|
||||
project,
|
||||
user,
|
||||
name: 'new-state',
|
||||
lock_id: lock_id
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
create(:terraform_state, :locked, project: project, name: 'new-state', lock_xid: 'abc-abc')
|
||||
end
|
||||
|
||||
it 'unlocks the state' do
|
||||
state = handler.unlock!
|
||||
|
||||
expect(state.lock_xid).to be_nil
|
||||
end
|
||||
|
||||
context 'with no lock ID (force-unlock)' do
|
||||
let(:lock_id) { }
|
||||
|
||||
it 'unlocks the state' do
|
||||
state = handler.unlock!
|
||||
|
||||
expect(state.lock_xid).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different lock ID' do
|
||||
let(:lock_id) { 'other' }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { handler.unlock! }
|
||||
.to raise_error(described_class::StateLockedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StubObjectStorage
|
||||
def stub_packages_object_storage(**params)
|
||||
stub_object_storage_uploader(config: ::Gitlab.config.packages.object_store,
|
||||
uploader: ::Packages::PackageFileUploader,
|
||||
remote_directory: 'packages',
|
||||
**params)
|
||||
end
|
||||
|
||||
def stub_dependency_proxy_object_storage(**params)
|
||||
stub_object_storage_uploader(config: ::Gitlab.config.dependency_proxy.object_store,
|
||||
uploader: ::DependencyProxy::FileUploader,
|
||||
remote_directory: 'dependency_proxy',
|
||||
**params)
|
||||
end
|
||||
|
||||
def stub_object_storage_pseudonymizer
|
||||
stub_object_storage(connection_params: Pseudonymizer::Uploader.object_store_credentials,
|
||||
remote_directory: Pseudonymizer::Uploader.remote_directory)
|
||||
end
|
||||
|
||||
def stub_object_storage_uploader(
|
||||
config:,
|
||||
uploader:,
|
||||
|
|
@ -89,8 +108,3 @@ module StubObjectStorage
|
|||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
require_relative '../../../ee/spec/support/helpers/ee/stub_object_storage' if
|
||||
Dir.exist?("#{__dir__}/../../../ee")
|
||||
|
||||
StubObjectStorage.prepend_if_ee('EE::StubObjectStorage')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rake_helper'
|
||||
|
||||
RSpec.describe 'gitlab:packages namespace rake task' do
|
||||
before :all do
|
||||
Rake.application.rake_require 'tasks/gitlab/packages/migrate'
|
||||
end
|
||||
|
||||
describe 'migrate' do
|
||||
let(:local) { ObjectStorage::Store::LOCAL }
|
||||
let(:remote) { ObjectStorage::Store::REMOTE }
|
||||
let!(:package_file) { create(:package_file, :pom, file_store: local) }
|
||||
|
||||
def packages_migrate
|
||||
run_rake_task('gitlab:packages:migrate')
|
||||
end
|
||||
|
||||
context 'object storage disabled' do
|
||||
before do
|
||||
stub_packages_object_storage(enabled: false)
|
||||
end
|
||||
|
||||
it "doesn't migrate files" do
|
||||
expect { packages_migrate }.to raise_error('Object store is disabled for packages feature')
|
||||
end
|
||||
end
|
||||
|
||||
context 'object storage enabled' do
|
||||
before do
|
||||
stub_packages_object_storage
|
||||
end
|
||||
|
||||
it 'migrates local file to object storage' do
|
||||
expect { packages_migrate }.to change { package_file.reload.file_store }.from(local).to(remote)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue