diff options
25 files changed, 610 insertions, 229 deletions
diff --git a/.overcommit.yml.example b/.overcommit.yml.example index 2cca4c0b488..e0f55dd4a14 100644 --- a/.overcommit.yml.example +++ b/.overcommit.yml.example @@ -44,6 +44,22 @@ PreCommit: # on_warn: fail # Treat all warnings as failures ScssLint: enabled: true + MarkdownLint: + enabled: true + description: 'Lint documentation for Markdown errors' + required_executable: 'node_modules/.bin/markdownlint' + flags: ['--config', '.markdownlint.json', 'doc/**/*.md'] + install_command: 'yarn install' + include: + - 'doc/**/*.md' + Vale: + enabled: true + description: 'Lint documentation for grammatical and formatting errors' + required_executable: 'vale' + flags: ['--config', '.vale.ini', '--minAlertLevel', 'error', 'doc'] + install_command: 'brew install vale # (or use another package manager)' + include: + - 'doc/**/*.md' CommitMsg: TextWidth: diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index e11b0cd67cc..1662e7923b7 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -4,6 +4,7 @@ import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui'; import { sprintf } from '~/locale'; import IssueMilestone from './issue_milestone.vue'; import IssueAssignees from './issue_assignees.vue'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; import relatedIssuableMixin from '../../mixins/related_issuable_mixin'; import CiIcon from '../ci_icon.vue'; @@ -15,6 +16,8 @@ export default { CiIcon, GlIcon, GlTooltip, + IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), + IssueDueDate, }, directives: { GlTooltip: GlTooltipDirective, @@ -120,8 +123,21 @@ export default { /> <!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue --> - <slot name="dueDate"></slot> - <slot name="weight"></slot> + <span v-if="weight > 0" class="order-md-1"> + <issue-weight + :weight="weight" + class="item-weight gl-display-flex gl-align-items-center" + tag-name="span" + /> + </span> + + <span v-if="dueDate" class="order-md-1"> + <issue-due-date + :date="dueDate" + tooltip-placement="top" + css-class="item-due-date gl-display-flex gl-align-items-center" + /> + </span> <issue-assignees v-if="hasAssignees" diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index 005390e60b6..7300411f073 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -311,10 +311,6 @@ content: '\f1b3'; } -.fa-times-circle::before { - content: '\f057'; -} - .fa-skype::before { content: '\f17e'; } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index d8084a20af9..1fc6ad62237 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -249,7 +249,7 @@ input[type='checkbox']:hover { .search-clear { position: absolute; right: 10px; - top: 10px; + top: 9px; padding: 0; color: $gray-darkest; line-height: 0; diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb index f6e068302c1..4d9b3f294ed 100644 --- a/app/presenters/packages/detail/package_presenter.rb +++ b/app/presenters/packages/detail/package_presenter.rb @@ -22,6 +22,7 @@ module Packages package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum package_detail[:nuget_metadatum] = @package.nuget_metadatum if @package.nuget_metadatum + package_detail[:composer_metadatum] = @package.composer_metadatum if @package.composer_metadatum package_detail[:dependency_links] = @package.dependency_links.map(&method(:build_dependency_links)) package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 63b6197a04d..ac1f022c63f 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -169,10 +169,6 @@ module ObjectStorage object_store_options.connection.to_hash.deep_symbolize_keys end - def consolidated_settings? - object_store_options.fetch('consolidated_settings', false) - end - def remote_store_path object_store_options.remote_directory end @@ -193,14 +189,18 @@ module ObjectStorage File.join(self.root, TMP_UPLOAD_PATH) end + def object_store_config + ObjectStorage::Config.new(object_store_options) + end + def workhorse_remote_upload_options(has_length:, maximum_size: nil) return unless self.object_store_enabled? return unless self.direct_upload_enabled? id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') upload_path = File.join(TMP_UPLOAD_PATH, id) - direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path, - has_length: has_length, maximum_size: maximum_size, consolidated_settings: consolidated_settings?) + direct_upload = ObjectStorage::DirectUpload.new(self.object_store_config, upload_path, + has_length: has_length, maximum_size: maximum_size) direct_upload.to_hash.merge(ID: id) end @@ -283,6 +283,10 @@ module ObjectStorage self.class.object_store_credentials end + def fog_attributes + @fog_attributes ||= self.class.object_store_config.fog_attributes + end + # Set ACL of uploaded objects to not-public (fog-aws)[1] or no ACL at all # (fog-google). Value is ignored by other supported backends (fog-aliyun, # fog-openstack, fog-rackspace) diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml index df615eb189a..47aa8faae97 100644 --- a/app/views/groups/runners/_runner.html.haml +++ b/app/views/groups/runners/_runner.html.haml @@ -72,10 +72,10 @@ .btn-group - if runner.active? = link_to pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do - = icon('pause') + = sprite_icon('pause') - else = link_to resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do - = icon('play') + = sprite_icon('play') - if runner.belongs_to_more_than_one_project? .btn-group .btn.btn-danger.has-tooltip{ 'aria-label' => 'Remove', 'data-container' => 'body', 'data-original-title' => _('Multi-project Runners cannot be removed'), 'data-placement' => 'top', disabled: 'disabled' } diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index dc75918eb93..de55c72f4f2 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -11,7 +11,7 @@ = search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false = icon("search", class: "search-icon") %button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" } - = icon("times-circle") + = sprite_icon('clear', size: 16) %span.sr-only = _("Clear search") - unless params[:snippets].eql? 'true' diff --git a/changelogs/unreleased/225924-225933-update-play-and-pause-icons.yml b/changelogs/unreleased/225924-225933-update-play-and-pause-icons.yml new file mode 100644 index 00000000000..d03baa9f30a --- /dev/null +++ b/changelogs/unreleased/225924-225933-update-play-and-pause-icons.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-play/pause icons with svg +merge_request: 38535 +author: +type: other diff --git a/changelogs/unreleased/nicolasdular-replace-fa-times.yml b/changelogs/unreleased/nicolasdular-replace-fa-times.yml new file mode 100644 index 00000000000..031b9e6a7c5 --- /dev/null +++ b/changelogs/unreleased/nicolasdular-replace-fa-times.yml @@ -0,0 +1,5 @@ +--- +title: Replace times-circle with GitLab SVG clear icon +merge_request: 38409 +author: +type: other diff --git a/changelogs/unreleased/sh-s3-encryption.yml b/changelogs/unreleased/sh-s3-encryption.yml new file mode 100644 index 00000000000..3a92728820d --- /dev/null +++ b/changelogs/unreleased/sh-s3-encryption.yml @@ -0,0 +1,5 @@ +--- +title: Add support for specifying AWS S3 Server Side Encryption (AWS-KMS) +merge_request: 38240 +author: +type: added diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 7ba256b39cd..e16a254e256 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -218,6 +218,9 @@ production: &base # region: us-east-1 # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4. # endpoint: 'https://s3.amazonaws.com' # default: nil - Useful for S3 compliant services such as DigitalOcean Spaces + # storage_options: + # server_side_encryption: AES256 # AES256, aws:kms + # server_side_encryption_kms_key_id: # Amazon Resource Name. See https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html # objects: # artifacts: # bucket: artifacts diff --git a/config/initializers/carrierwave_patch.rb b/config/initializers/carrierwave_patch.rb new file mode 100644 index 00000000000..94a79e5990d --- /dev/null +++ b/config/initializers/carrierwave_patch.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "carrierwave/storage/fog" + +# This pulls in https://github.com/carrierwaveuploader/carrierwave/pull/2504 to support +# sending AWS S3 encryption headers when copying objects. +module CarrierWave + module Storage + class Fog < Abstract + class File + def copy_to(new_path) + connection.copy_object(@uploader.fog_directory, file.key, @uploader.fog_directory, new_path, copy_to_options) + CarrierWave::Storage::Fog::File.new(@uploader, @base, new_path) + end + + def copy_to_options + acl_header.merge(@uploader.fog_attributes) + end + end + end + end +end diff --git a/config/object_store_settings.rb b/config/object_store_settings.rb index d8e1939a346..0d346135463 100644 --- a/config/object_store_settings.rb +++ b/config/object_store_settings.rb @@ -13,6 +13,7 @@ class ObjectStoreSettings object_store['direct_upload'] = false if object_store['direct_upload'].nil? object_store['background_upload'] = true if object_store['background_upload'].nil? object_store['proxy_download'] = false if object_store['proxy_download'].nil? + object_store['storage_options'] ||= {} # Convert upload connection settings to use string keys, to make Fog happy object_store['connection']&.deep_stringify_keys! @@ -37,6 +38,8 @@ class ObjectStoreSettings # region: gdk # endpoint: 'http://127.0.0.1:9000' # path_style: true + # storage_options: + # server_side_encryption: AES256 # proxy_download: true # objects: # artifacts: @@ -49,7 +52,7 @@ class ObjectStoreSettings # # Settings.artifacts['object_store'] = { # "enabled" => true, - # "connection"=> { + # "connection" => { # "provider" => "AWS", # "aws_access_key_id" => "minio", # "aws_secret_access_key" => "gdk-minio", @@ -57,6 +60,9 @@ class ObjectStoreSettings # "endpoint" => "http://127.0.0.1:9000", # "path_style" => true # }, + # "storage_options" => { + # "server_side_encryption" => "AES256" + # }, # "direct_upload" => true, # "background_upload" => false, # "proxy_download" => false, @@ -73,6 +79,9 @@ class ObjectStoreSettings # "endpoint" => "http://127.0.0.1:9000", # "path_style" => true # }, + # "storage_options" => { + # "server_side_encryption" => "AES256" + # }, # "direct_upload" => true, # "background_upload" => false, # "proxy_download" => true, @@ -91,12 +100,13 @@ class ObjectStoreSettings return unless use_consolidated_settings? main_config = settings['object_store'] - common_config = main_config.slice('enabled', 'connection', 'proxy_download') + common_config = main_config.slice('enabled', 'connection', 'proxy_download', 'storage_options') # Convert connection settings to use string keys, to make Fog happy common_config['connection']&.deep_stringify_keys! # These are no longer configurable if common config is used common_config['direct_upload'] = true common_config['background_upload'] = false + common_config['storage_options'] ||= {} SUPPORTED_TYPES.each do |store_type| overrides = main_config.dig('objects', store_type) || {} diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index dfee30d256d..61a45d163b5 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -387,6 +387,7 @@ reverify Rubix Rubocop Rubular +ruleset runbook runbooks runit diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md index ed254052180..f6e64c1f1e6 100644 --- a/doc/development/contributing/style_guides.md +++ b/doc/development/contributing/style_guides.md @@ -10,26 +10,23 @@ we suggest investigating to see if a plugin exists. For instance here is the ## Pre-commit static analysis -You're strongly advised to install -[Overcommit](https://github.com/sds/overcommit) to automatically check for +You should install [`overcommit`](https://github.com/sds/overcommit) to automatically check for static analysis offenses before committing locally. -In your GitLab source directory run: +After installing `overcommit`, run the following in your GitLab source directory: ```shell make -C tooling/overcommit ``` -Then before a commit is created, Overcommit will automatically check for -RuboCop (and other checks) offenses on every modified file. +Then before a commit is created, `overcommit` automatically checks for RuboCop (and other checks) +offenses on every modified file. -This saves you time as you don't have to wait for the same errors to be detected -by the CI. +This saves you time as you don't have to wait for the same errors to be detected by CI/CD. -Overcommit relies on a pre-commit hook to prevent commits that violate its ruleset. -If you wish to override this behavior, it can be done by passing the ENV variable -`OVERCOMMIT_DISABLE`; i.e. `OVERCOMMIT_DISABLE=1 git rebase master` to rebase while -disabling the Git hook. +`overcommit` relies on a pre-commit hook to prevent commits that violate its ruleset. To override +this behavior, pass the `OVERCOMMIT_DISABLE` environment variable. For example, +`OVERCOMMIT_DISABLE=1 git rebase master` to rebase while disabling the Git hook. ## Ruby, Rails, RSpec diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index ad9ad8c64cc..283060ba8d4 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -624,6 +624,7 @@ You can use markdownlint: - [On the command line](https://github.com/igorshubovych/markdownlint-cli#markdownlint-cli--). - [Within a code editor](#configure-editors). +- [In a `pre-commit` hook](#configure-pre-commit-hooks). #### Vale @@ -650,6 +651,9 @@ You can use Vale: - [On the command line](https://errata-ai.gitbook.io/vale/getting-started/usage). - [Within a code editor](#configure-editors). +- [In a `pre-commit` hook](#configure-pre-commit-hooks). Vale only reports errors in the + `pre-commit` hook (the same configuration as the CI/CD pipelines), and does not report suggestions + or warnings. #### Install linters @@ -703,6 +707,22 @@ To configure Vale within your editor, install one of the following as appropriat We don't use [Vale Server](https://errata-ai.github.io/vale/#using-vale-with-a-text-editor-or-another-third-party-application). +#### Configure pre-commit hooks + +Git [pre-commit hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) allow Git users to +run tests or other processes before committing to a branch, with the ability to not commit to the branch if +failures occur with these tests. + +[`overcommit`](https://github.com/sds/overcommit) is a Git hooks manager, making configuring, +installing, and removing Git hooks easy. + +Sample configuration for `overcommit` is available in the +[`.overcommit.yml.example`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.overcommit.yml.example) +file for the [`gitlab`](https://gitlab.com/gitlab-org/gitlab) project. + +To set up `overcommit` for documentation linting, see +[Pre-commit static analysis](../contributing/style_guides.md#pre-commit-static-analysis). + #### Disable Vale tests You can disable a specific Vale linting rule or all Vale linting rules for any portion of a diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb new file mode 100644 index 00000000000..489b85104d6 --- /dev/null +++ b/lib/object_storage/config.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module ObjectStorage + class Config + attr_reader :options + + def initialize(options) + @options = options.to_hash.deep_symbolize_keys + end + + def credentials + @credentials ||= options[:connection] || {} + end + + def storage_options + @storage_options ||= options[:storage_options] || {} + end + + def enabled? + options[:enabled] + end + + def bucket + options[:remote_directory] + end + + def consolidated_settings? + options.fetch(:consolidated_settings, false) + end + + # AWS-specific options + def aws? + provider == 'AWS' + end + + def use_iam_profile? + credentials.fetch(:use_iam_profile, false) + end + + def use_path_style? + credentials.fetch(:path_style, false) + end + + def server_side_encryption + storage_options[:server_side_encryption] + end + + def server_side_encryption_kms_key_id + storage_options[:server_side_encryption_kms_key_id] + end + + def provider + credentials[:provider].to_s + end + # End AWS-specific options + + def google? + provider == 'Google' + end + + def fog_attributes + @fog_attributes ||= begin + return {} unless enabled? && aws? + return {} unless server_side_encryption.present? + + aws_server_side_encryption_headers.compact + end + end + + private + + def aws_server_side_encryption_headers + { + 'x-amz-server-side-encryption' => server_side_encryption, + 'x-amz-server-side-encryption-aws-kms-key-id' => server_side_encryption_kms_key_id + } + end + end +end diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index 76f92f62e9c..5784a089bba 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -22,20 +22,20 @@ module ObjectStorage MAXIMUM_MULTIPART_PARTS = 100 MINIMUM_MULTIPART_SIZE = 5.megabytes - attr_reader :credentials, :bucket_name, :object_name - attr_reader :has_length, :maximum_size, :consolidated_settings + attr_reader :config, :credentials, :bucket_name, :object_name + attr_reader :has_length, :maximum_size - def initialize(credentials, bucket_name, object_name, has_length:, maximum_size: nil, consolidated_settings: false) + def initialize(config, object_name, has_length:, maximum_size: nil) unless has_length raise ArgumentError, 'maximum_size has to be specified if length is unknown' unless maximum_size end - @credentials = credentials - @bucket_name = bucket_name + @config = config + @credentials = config.credentials + @bucket_name = config.bucket @object_name = object_name @has_length = has_length @maximum_size = maximum_size - @consolidated_settings = consolidated_settings end def to_hash @@ -62,7 +62,7 @@ module ObjectStorage end def workhorse_client_hash - return {} unless aws? + return {} unless config.aws? { UseWorkhorseClient: use_workhorse_s3_client?, @@ -73,16 +73,18 @@ module ObjectStorage Bucket: bucket_name, Region: credentials[:region], Endpoint: credentials[:endpoint], - PathStyle: credentials.fetch(:path_style, false), - UseIamProfile: credentials.fetch(:use_iam_profile, false) - } + PathStyle: config.use_path_style?, + UseIamProfile: config.use_iam_profile?, + ServerSideEncryption: config.server_side_encryption, + SSEKMSKeyID: config.server_side_encryption_kms_key_id + }.compact } } end def use_workhorse_s3_client? return false unless Feature.enabled?(:use_workhorse_s3_client, default_enabled: true) - return false unless credentials.fetch(:use_iam_profile, false) || consolidated_settings + return false unless config.use_iam_profile? || config.consolidated_settings? # The Golang AWS SDK does not support V2 signatures return false unless credentials.fetch(:aws_signature_version, 4).to_i >= 4 @@ -95,7 +97,7 @@ module ObjectStorage # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html def get_url - if google? + if config.google? connection.get_object_https_url(bucket_name, object_name, expire_at) else connection.get_object_url(bucket_name, object_name, expire_at) @@ -169,23 +171,15 @@ module ObjectStorage ].min end - def aws? - provider == 'AWS' - end - - def google? - provider == 'Google' - end - def requires_multipart_upload? - aws? && !has_length + config.aws? && !has_length end def upload_id return unless requires_multipart_upload? strong_memoize(:upload_id) do - new_upload = connection.initiate_multipart_upload(bucket_name, object_name) + new_upload = connection.initiate_multipart_upload(bucket_name, object_name, config.fog_attributes) new_upload.body["UploadId"] end end diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js index fe9a5156539..c6c21034a79 100644 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js @@ -1,7 +1,7 @@ -import Vue from 'vue'; import { mount } from '@vue/test-utils'; import { formatDate } from '~/lib/utils/datetime_utility'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data'; import { TEST_HOST } from 'jest/helpers/test_constants'; @@ -71,85 +71,65 @@ describe('RelatedIssuableItem', () => { }); describe('token state', () => { - let tokenState; + const tokenState = () => wrapper.find({ ref: 'iconElementXL' }); - beforeEach(done => { + beforeEach(() => { wrapper.setProps({ state: 'opened' }); - - Vue.nextTick(() => { - tokenState = wrapper.find('.issue-token-state-icon-open'); - - done(); - }); }); it('renders if hasState', () => { - expect(tokenState.exists()).toBe(true); + expect(tokenState().exists()).toBe(true); }); it('renders state title', () => { - const stateTitle = tokenState.attributes('title'); + const stateTitle = tokenState().attributes('title'); const formattedCreateDate = formatDate(props.createdAt); expect(stateTitle).toContain('<span class="bold">Opened</span>'); - expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`); }); it('renders aria label', () => { - expect(tokenState.attributes('aria-label')).toEqual('opened'); + expect(tokenState().attributes('aria-label')).toEqual('opened'); }); it('renders open icon when open state', () => { - expect(tokenState.classes('issue-token-state-icon-open')).toBe(true); + expect(tokenState().classes('issue-token-state-icon-open')).toBe(true); }); - it('renders close icon when close state', done => { + it('renders close icon when close state', async () => { wrapper.setProps({ state: 'closed', closedAt: '2018-12-01T00:00:00.00Z', }); + await wrapper.vm.$nextTick(); - Vue.nextTick(() => { - expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true); - - done(); - }); + expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true); }); }); describe('token metadata', () => { - let tokenMetadata; - - beforeEach(done => { - Vue.nextTick(() => { - tokenMetadata = wrapper.find('.item-meta'); - - done(); - }); - }); + const tokenMetadata = () => wrapper.find('.item-meta'); it('renders item path and ID', () => { - const pathAndID = tokenMetadata.find('.item-path-id').text(); + const pathAndID = tokenMetadata() + .find('.item-path-id') + .text(); expect(pathAndID).toContain('gitlab-org/gitlab-test'); expect(pathAndID).toContain('#1'); }); it('renders milestone icon and name', () => { - const milestoneIcon = tokenMetadata.find('.item-milestone svg use'); - const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title'); + const milestoneIcon = tokenMetadata().find('.item-milestone svg use'); + const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title'); expect(milestoneIcon.attributes('href')).toContain('clock'); expect(milestoneTitle.text()).toContain('Milestone title'); }); - it('renders due date component', () => { - expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true); - }); - - it('renders weight component', () => { - expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true); + it('renders due date component with correct due date', () => { + expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate); }); }); @@ -163,40 +143,30 @@ describe('RelatedIssuableItem', () => { }); describe('remove button', () => { - let removeBtn; + const removeButton = () => wrapper.find({ ref: 'removeButton' }); - beforeEach(done => { + beforeEach(() => { wrapper.setProps({ canRemove: true }); - Vue.nextTick(() => { - removeBtn = wrapper.find({ ref: 'removeButton' }); - - done(); - }); }); it('renders if canRemove', () => { - expect(removeBtn.exists()).toBe(true); + expect(removeButton().exists()).toBe(true); }); - it('renders disabled button when removeDisabled', done => { - wrapper.vm.removeDisabled = true; + it('renders disabled button when removeDisabled', async () => { + wrapper.setData({ removeDisabled: true }); + await wrapper.vm.$nextTick(); - Vue.nextTick(() => { - expect(removeBtn.attributes('disabled')).toEqual('disabled'); - - done(); - }); + expect(removeButton().attributes('disabled')).toEqual('disabled'); }); - it('triggers onRemoveRequest when clicked', () => { - removeBtn.trigger('click'); + it('triggers onRemoveRequest when clicked', async () => { + removeButton().trigger('click'); + await wrapper.vm.$nextTick(); + const { relatedIssueRemoveRequest } = wrapper.emitted(); - return wrapper.vm.$nextTick().then(() => { - const { relatedIssueRemoveRequest } = wrapper.emitted(); - - expect(relatedIssueRemoveRequest.length).toBe(1); - expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]); - }); + expect(relatedIssueRemoveRequest.length).toBe(1); + expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]); }); }); }); diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb new file mode 100644 index 00000000000..6cbd96608dd --- /dev/null +++ b/spec/lib/object_storage/config_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe ObjectStorage::Config do + let(:region) { 'us-east-1' } + let(:bucket_name) { 'test-bucket' } + let(:path_style) { false } + let(:use_iam_profile) { false } + let(:credentials) do + { + provider: 'AWS', + aws_access_key_id: 'AWS_ACCESS_KEY_ID', + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY', + region: region, + path_style: path_style, + use_iam_profile: use_iam_profile + } + end + + let(:storage_options) do + { + server_side_encryption: 'AES256', + server_side_encryption_kms_key_id: 'arn:aws:12345' + } + end + + let(:raw_config) do + { + enabled: true, + connection: credentials, + remote_directory: bucket_name, + storage_options: storage_options + } + end + + subject { described_class.new(raw_config.as_json) } + + describe '#credentials' do + it { expect(subject.credentials).to eq(credentials) } + end + + describe '#storage_options' do + it { expect(subject.storage_options).to eq(storage_options) } + end + + describe '#enabled?' do + it { expect(subject.enabled?).to eq(true) } + end + + describe '#bucket' do + it { expect(subject.bucket).to eq(bucket_name) } + end + + context 'with unconsolidated settings' do + describe 'consolidated_settings? returns false' do + it { expect(subject.consolidated_settings?).to be false } + end + end + + context 'with consolidated settings' do + before do + raw_config[:consolidated_settings] = true + end + + describe 'consolidated_settings? returns true' do + it { expect(subject.consolidated_settings?).to be true } + end + end + + context 'with IAM profile in use' do + let(:use_iam_profile) { true } + + it '#use_iam_profile? returns true' do + expect(subject.use_iam_profile?).to be true + end + end + + context 'with IAM profile not in use' do + it '#use_iam_profile? returns false' do + expect(subject.use_iam_profile?).to be false + end + end + + context 'with path style' do + let(:path_style) { true } + + it '#use_path_style? returns true' do + expect(subject.use_path_style?).to be true + end + end + + context 'with hostname style access' do + it '#use_path_style? returns false' do + expect(subject.use_path_style?).to be false + end + end + + context 'with AWS credentials' do + it { expect(subject.provider).to eq('AWS') } + it { expect(subject.aws?).to be true } + it { expect(subject.google?).to be false } + end + + context 'with Google credentials' do + let(:credentials) do + { + provider: 'Google', + google_client_email: 'foo@gcp-project.example.com', + google_json_key_location: '/path/to/gcp.json' + } + end + + it { expect(subject.provider).to eq('Google') } + it { expect(subject.aws?).to be false } + it { expect(subject.google?).to be true } + it { expect(subject.fog_attributes).to eq({}) } + end + + context 'with SSE-KMS enabled' do + it { expect(subject.server_side_encryption).to eq('AES256') } + it { expect(subject.server_side_encryption_kms_key_id).to eq('arn:aws:12345') } + it { expect(subject.fog_attributes.keys).to match_array(%w(x-amz-server-side-encryption x-amz-server-side-encryption-aws-kms-key-id)) } + end + + context 'with only server side encryption enabled' do + let(:storage_options) { { server_side_encryption: 'AES256' } } + + it { expect(subject.server_side_encryption).to eq('AES256') } + it { expect(subject.server_side_encryption_kms_key_id).to be_nil } + it { expect(subject.fog_attributes).to eq({ 'x-amz-server-side-encryption' => 'AES256' }) } + end + + context 'without encryption enabled' do + let(:storage_options) { {} } + + it { expect(subject.server_side_encryption).to be_nil } + it { expect(subject.server_side_encryption_kms_key_id).to be_nil } + it { expect(subject.fog_attributes).to eq({}) } + end + + context 'with object storage disabled' do + before do + raw_config['enabled'] = false + end + + it { expect(subject.enabled?).to be false } + it { expect(subject.fog_attributes).to eq({}) } + end +end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 1c1455e2456..7e0f31cbd23 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -18,13 +18,25 @@ RSpec.describe ObjectStorage::DirectUpload do } end + let(:storage_options) { {} } + let(:raw_config) do + { + enabled: true, + connection: credentials, + remote_directory: bucket_name, + storage_options: storage_options, + consolidated_settings: consolidated_settings + } + end + + let(:config) { ObjectStorage::Config.new(raw_config) } let(:storage_url) { 'https://uploads.s3.amazonaws.com/' } let(:bucket_name) { 'uploads' } let(:object_name) { 'tmp/uploads/my-file' } let(:maximum_size) { 1.gigabyte } - let(:direct_upload) { described_class.new(credentials, bucket_name, object_name, has_length: has_length, maximum_size: maximum_size, consolidated_settings: consolidated_settings) } + let(:direct_upload) { described_class.new(config, object_name, has_length: has_length, maximum_size: maximum_size) } before do Fog.unmock! @@ -62,7 +74,7 @@ RSpec.describe ObjectStorage::DirectUpload do end describe '#get_url' do - subject { described_class.new(credentials, bucket_name, object_name, has_length: true) } + subject { described_class.new(config, object_name, has_length: true) } context 'when AWS is used' do it 'calls the proper method' do @@ -111,6 +123,7 @@ RSpec.describe ObjectStorage::DirectUpload do expect(s3_config[:Region]).to eq(region) expect(s3_config[:PathStyle]).to eq(path_style) expect(s3_config[:UseIamProfile]).to eq(use_iam_profile) + expect(s3_config.keys).not_to include(%i(ServerSideEncryption SSEKMSKeyID)) end context 'when feature flag is disabled' do @@ -150,6 +163,33 @@ RSpec.describe ObjectStorage::DirectUpload do expect(subject[:UseWorkhorseClient]).to be true end end + + context 'when only server side encryption is used' do + let(:storage_options) { { server_side_encryption: 'AES256' } } + + it 'sends server side encryption settings' do + s3_config = subject[:ObjectStorage][:S3Config] + + expect(s3_config[:ServerSideEncryption]).to eq('AES256') + expect(s3_config.keys).not_to include(:SSEKMSKeyID) + end + end + + context 'when SSE-KMS is used' do + let(:storage_options) do + { + server_side_encryption: 'AES256', + server_side_encryption_kms_key_id: 'arn:aws:12345' + } + end + + it 'sends server side encryption settings' do + s3_config = subject[:ObjectStorage][:S3Config] + + expect(s3_config[:ServerSideEncryption]).to eq('AES256') + expect(s3_config[:SSEKMSKeyID]).to eq('arn:aws:12345') + end + end end shared_examples 'a valid Google upload' do diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb index 34582957364..732d8d0f6ad 100644 --- a/spec/presenters/packages/detail/package_presenter_spec.rb +++ b/spec/presenters/packages/detail/package_presenter_spec.rb @@ -10,13 +10,14 @@ RSpec.describe ::Packages::Detail::PackagePresenter do let_it_be(:user_info) { { name: user.name, avatar_url: user.avatar_url } } let!(:expected_package_files) do - npm_file = package.package_files.first - [{ - created_at: npm_file.created_at, - download_path: npm_file.download_path, - file_name: npm_file.file_name, - size: npm_file.size - }] + package.package_files.map do |file| + { + created_at: file.created_at, + download_path: file.download_path, + file_name: file.file_name, + size: file.size + } + end end let(:pipeline_info) do pipeline = package.build_info.pipeline @@ -67,6 +68,15 @@ RSpec.describe ::Packages::Detail::PackagePresenter do end end + context 'with composer metadata' do + let(:package) { create(:composer_package, :with_metadatum, sha: '123', project: project) } + let(:expected_package_details) { super().merge(composer_metadatum: package.composer_metadatum) } + + it 'returns composer_metadatum' do + expect(presenter.detail_view).to eq expected_package_details + end + end + context 'with nuget_metadatum' do let_it_be(:package) { create(:nuget_package, project: project) } let_it_be(:nuget_metadatum) { create(:nuget_metadatum, package: package) } diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb index 7c7e8da3fb1..e02f6099637 100644 --- a/spec/requests/api/group_packages_spec.rb +++ b/spec/requests/api/group_packages_spec.rb @@ -13,135 +13,133 @@ RSpec.describe API::GroupPackages do let(:url) { "/groups/#{group.id}/packages" } let(:package_schema) { 'public_api/v4/packages/group_packages' } - context 'without the need for a license' do - context 'with sorting' do - let_it_be(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } - let_it_be(:package2) { create(:nuget_package, project: project, version: '2.0.4') } - let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } - - before do - travel_to(1.day.ago) do - package3 - end + context 'with sorting' do + let_it_be(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } + let_it_be(:package2) { create(:nuget_package, project: project, version: '2.0.4') } + let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } + + before do + travel_to(1.day.ago) do + package3 end + end - context 'without sorting params' do - let(:packages) { [package3, package1, package2] } + context 'without sorting params' do + let(:packages) { [package3, package1, package2] } - it 'sorts by created_at asc' do - subject + it 'sorts by created_at asc' do + subject - expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) - end + expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) end + end - it_behaves_like 'package sorting', 'name' do - let(:packages) { [package1, package2, package3] } - end + it_behaves_like 'package sorting', 'name' do + let(:packages) { [package1, package2, package3] } + end - it_behaves_like 'package sorting', 'created_at' do - let(:packages) { [package3, package1, package2] } - end + it_behaves_like 'package sorting', 'created_at' do + let(:packages) { [package3, package1, package2] } + end - it_behaves_like 'package sorting', 'version' do - let(:packages) { [package3, package2, package1] } - end + it_behaves_like 'package sorting', 'version' do + let(:packages) { [package3, package2, package1] } + end - it_behaves_like 'package sorting', 'type' do - let(:packages) { [package3, package1, package2] } - end + it_behaves_like 'package sorting', 'type' do + let(:packages) { [package3, package1, package2] } + end - it_behaves_like 'package sorting', 'project_path' do - let(:another_project) { create(:project, :public, namespace: group, name: 'project B') } - let!(:package4) { create(:npm_package, project: another_project, version: '3.1.0', name: "@#{project.root_namespace.path}/bar") } + it_behaves_like 'package sorting', 'project_path' do + let(:another_project) { create(:project, :public, namespace: group, name: 'project B') } + let!(:package4) { create(:npm_package, project: another_project, version: '3.1.0', name: "@#{project.root_namespace.path}/bar") } - let(:packages) { [package1, package2, package3, package4] } - end + let(:packages) { [package1, package2, package3, package4] } end + end - context 'with private group' do - let!(:package1) { create(:package, project: project) } - let!(:package2) { create(:package, project: project) } + context 'with private group' do + let!(:package1) { create(:package, project: project) } + let!(:package2) { create(:package, project: project) } - let(:group) { create(:group, :private) } - let(:subgroup) { create(:group, :private, parent: group) } - let(:project) { create(:project, :private, namespace: group) } - let(:subproject) { create(:project, :private, namespace: subgroup) } + let(:group) { create(:group, :private) } + let(:subgroup) { create(:group, :private, parent: group) } + let(:project) { create(:project, :private, namespace: group) } + let(:subproject) { create(:project, :private, namespace: subgroup) } - context 'with unauthenticated user' do - it_behaves_like 'rejects packages access', :group, :no_type, :not_found - end + context 'with unauthenticated user' do + it_behaves_like 'rejects packages access', :group, :no_type, :not_found + end + + context 'with authenticated user' do + subject { get api(url, user) } + + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'rejects packages access', :group, :guest, :forbidden - context 'with authenticated user' do - subject { get api(url, user) } + context 'with subgroup' do + let(:subgroup) { create(:group, :private, parent: group) } + let(:subproject) { create(:project, :private, namespace: subgroup) } + let!(:package3) { create(:npm_package, project: subproject) } - it_behaves_like 'returns packages', :group, :owner - it_behaves_like 'returns packages', :group, :maintainer - it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'returns packages with subgroups', :group, :owner + it_behaves_like 'returns packages with subgroups', :group, :maintainer + it_behaves_like 'returns packages with subgroups', :group, :developer it_behaves_like 'rejects packages access', :group, :reporter, :forbidden it_behaves_like 'rejects packages access', :group, :guest, :forbidden - context 'with subgroup' do - let(:subgroup) { create(:group, :private, parent: group) } - let(:subproject) { create(:project, :private, namespace: subgroup) } - let!(:package3) { create(:npm_package, project: subproject) } + context 'excluding subgroup' do + let(:url) { "/groups/#{group.id}/packages?exclude_subgroups=true" } - it_behaves_like 'returns packages with subgroups', :group, :owner - it_behaves_like 'returns packages with subgroups', :group, :maintainer - it_behaves_like 'returns packages with subgroups', :group, :developer + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer it_behaves_like 'rejects packages access', :group, :reporter, :forbidden it_behaves_like 'rejects packages access', :group, :guest, :forbidden - - context 'excluding subgroup' do - let(:url) { "/groups/#{group.id}/packages?exclude_subgroups=true" } - - it_behaves_like 'returns packages', :group, :owner - it_behaves_like 'returns packages', :group, :maintainer - it_behaves_like 'returns packages', :group, :developer - it_behaves_like 'rejects packages access', :group, :reporter, :forbidden - it_behaves_like 'rejects packages access', :group, :guest, :forbidden - end end end end + end - context 'with public group' do - let_it_be(:package1) { create(:package, project: project) } - let_it_be(:package2) { create(:package, project: project) } + context 'with public group' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } - context 'with unauthenticated user' do - it_behaves_like 'returns packages', :group, :no_type - end + context 'with unauthenticated user' do + it_behaves_like 'returns packages', :group, :no_type + end - context 'with authenticated user' do - subject { get api(url, user) } + context 'with authenticated user' do + subject { get api(url, user) } - it_behaves_like 'returns packages', :group, :owner - it_behaves_like 'returns packages', :group, :maintainer - it_behaves_like 'returns packages', :group, :developer - it_behaves_like 'returns packages', :group, :reporter - it_behaves_like 'returns packages', :group, :guest - end + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'returns packages', :group, :reporter + it_behaves_like 'returns packages', :group, :guest end + end - context 'with pagination params' do - let_it_be(:package1) { create(:package, project: project) } - let_it_be(:package2) { create(:package, project: project) } - let_it_be(:package3) { create(:npm_package, project: project) } - let_it_be(:package4) { create(:npm_package, project: project) } + context 'with pagination params' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } + let_it_be(:package3) { create(:npm_package, project: project) } + let_it_be(:package4) { create(:npm_package, project: project) } - it_behaves_like 'returns paginated packages' - end + it_behaves_like 'returns paginated packages' + end - it_behaves_like 'filters on each package_type', is_project: false + it_behaves_like 'filters on each package_type', is_project: false - context 'does not accept non supported package_type value' do - include_context 'package filter context' + context 'does not accept non supported package_type value' do + include_context 'package filter context' - let(:url) { group_filter_url(:type, 'foo') } + let(:url) { group_filter_url(:type, 'foo') } - it_behaves_like 'returning response status', :bad_request - end + it_behaves_like 'returning response status', :bad_request end end end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 694aafe5ed5..12c936e154b 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -382,6 +382,32 @@ RSpec.describe ObjectStorage do it { is_expected.to eq(nil) } end + describe '#fog_attributes' do + subject { uploader.fog_attributes } + + it { is_expected.to eq({}) } + + context 'with encryption configured' do + let(:raw_options) do + { + "enabled" => true, + "connection" => { "provider" => 'AWS' }, + "storage_options" => { "server_side_encryption" => "AES256" } + } + end + + let(:options) { Settingslogic.new(raw_options) } + + before do + allow(uploader_class).to receive(:options) do + double(object_store: options) + end + end + + it { is_expected.to eq({ "x-amz-server-side-encryption" => "AES256" }) } + end + end + describe '.workhorse_authorize' do let(:has_length) { true } let(:maximum_size) { nil } @@ -459,13 +485,18 @@ RSpec.describe ObjectStorage do context 'uses AWS' do let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" } + let(:credentials) do + { + provider: "AWS", + aws_access_key_id: "AWS_ACCESS_KEY_ID", + aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", + region: "eu-central-1" + } + end before do - expect(uploader_class).to receive(:object_store_credentials) do - { provider: "AWS", - aws_access_key_id: "AWS_ACCESS_KEY_ID", - aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", - region: "eu-central-1" } + expect_next_instance_of(ObjectStorage::Config) do |instance| + allow(instance).to receive(:credentials).and_return(credentials) end end @@ -502,12 +533,17 @@ RSpec.describe ObjectStorage do context 'uses Google' do let(:storage_url) { "https://storage.googleapis.com/uploads/" } + let(:credentials) do + { + provider: "Google", + google_storage_access_key_id: 'ACCESS_KEY_ID', + google_storage_secret_access_key: 'SECRET_ACCESS_KEY' + } + end before do - expect(uploader_class).to receive(:object_store_credentials) do - { provider: "Google", - google_storage_access_key_id: 'ACCESS_KEY_ID', - google_storage_secret_access_key: 'SECRET_ACCESS_KEY' } + expect_next_instance_of(ObjectStorage::Config) do |instance| + allow(instance).to receive(:credentials).and_return(credentials) end end @@ -537,15 +573,18 @@ RSpec.describe ObjectStorage do context 'uses GDK/minio' do let(:storage_url) { "http://minio:9000/uploads/" } + let(:credentials) do + { provider: "AWS", + aws_access_key_id: "AWS_ACCESS_KEY_ID", + aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", + endpoint: 'http://minio:9000', + path_style: true, + region: "gdk" } + end before do - expect(uploader_class).to receive(:object_store_credentials) do - { provider: "AWS", - aws_access_key_id: "AWS_ACCESS_KEY_ID", - aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", - endpoint: 'http://minio:9000', - path_style: true, - region: "gdk" } + expect_next_instance_of(ObjectStorage::Config) do |instance| + allow(instance).to receive(:credentials).and_return(credentials) end end |