Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-22 06:08:52 +00:00
parent 6ef43e2aa1
commit 16e3c34cac
17 changed files with 237 additions and 60 deletions

View File

@ -23,3 +23,23 @@ export const textColorForBackground = (backgroundColor) => {
}
return '#FFFFFF';
};
/**
* Check whether a color matches the expected hex format
*
* This matches any hex (0-9 and A-F) value which is either 3 or 6 characters in length
*
* An empty string will return `null` which means that this is neither valid nor invalid.
* This is useful for forms resetting the validation state
*
* @param color string = ''
*
* @returns {null|boolean}
*/
export const validateHexColor = (color = '') => {
if (!color) {
return null;
}
return /^#([0-9A-F]{3}){1,2}$/i.test(color);
};

View File

@ -3,12 +3,16 @@
* Renders a color picker input with preset colors to choose from
*
* @example
* <color-picker :label="__('Background color')" set-color="#FF0000" />
* <color-picker
:invalid-feedback="__('Please enter a valid hex (#RRGGBB or #RGB) color value')"
:label="__('Background color')"
set-color="#FF0000"
state="isValidColor"
/>
*/
import { GlFormGroup, GlFormInput, GlFormInputGroup, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const VALID_RGB_HEX_COLOR = /^#([0-9A-F]{3}){1,2}$/i;
const PREVIEW_COLOR_DEFAULT_CLASSES =
'gl-relative gl-w-7 gl-bg-gray-10 gl-rounded-top-left-base gl-rounded-bottom-left-base';
@ -24,6 +28,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
invalidFeedback: {
type: String,
required: false,
default: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
label: {
type: String,
required: false,
@ -34,6 +43,11 @@ export default {
required: false,
default: '',
},
state: {
type: Boolean,
required: false,
default: null,
},
},
data() {
return {
@ -50,46 +64,32 @@ export default {
return gon.suggested_label_colors;
},
previewColor() {
if (this.isValidColor) {
if (this.state) {
return { backgroundColor: this.selectedColor };
}
return {};
},
previewColorClasses() {
const borderStyle = this.isInvalidColor
? 'gl-inset-border-1-red-500'
: 'gl-inset-border-1-gray-400';
const borderStyle =
this.state === false ? 'gl-inset-border-1-red-500' : 'gl-inset-border-1-gray-400';
return `${PREVIEW_COLOR_DEFAULT_CLASSES} ${borderStyle}`;
},
hasSuggestedColors() {
return Object.keys(this.suggestedColors).length;
},
isInvalidColor() {
return this.isValidColor === false;
},
isValidColor() {
if (this.selectedColor === '') {
return null;
}
return VALID_RGB_HEX_COLOR.test(this.selectedColor);
},
},
methods: {
handleColorChange(color) {
this.selectedColor = color.trim();
if (this.isValidColor) {
this.$emit('input', this.selectedColor);
}
this.$emit('input', this.selectedColor);
},
},
i18n: {
fullDescription: __('Choose any color. Or you can choose one of the suggested colors below'),
shortDescription: __('Choose any color'),
invalid: __('Please enter a valid hex (#RRGGBB or #RGB) color value'),
},
};
</script>
@ -100,17 +100,17 @@ export default {
:label="label"
label-for="color-picker"
:description="description"
:invalid-feedback="this.$options.i18n.invalid"
:state="isValidColor"
:invalid-feedback="invalidFeedback"
:state="state"
:class="{ 'gl-mb-3!': hasSuggestedColors }"
>
<gl-form-input-group
id="color-picker"
:state="isValidColor"
max-length="7"
type="text"
class="gl-align-center gl-rounded-0 gl-rounded-top-right-base gl-rounded-bottom-right-base"
:value="selectedColor"
:state="state"
@input="handleColorChange"
>
<template #prepend>

View File

@ -9,6 +9,8 @@ module Terraform
belongs_to :build, class_name: 'Ci::Build', optional: true, foreign_key: :ci_build_id
scope :ordered_by_version_desc, -> { order(version: :desc) }
scope :with_files_stored_locally, -> { where(file_store: Terraform::StateUploader::Store::LOCAL) }
scope :preload_state, -> { includes(:terraform_state) }
default_value_for(:file_store) { StateUploader.default_store }

View File

@ -6,6 +6,10 @@ module Terraform
storage_options Gitlab.config.terraform_state
# TODO: Remove this line
# See https://gitlab.com/gitlab-org/gitlab/-/issues/232917
alias_method :upload, :model
delegate :terraform_state, :project_id, to: :model
# Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks)

View File

@ -0,0 +1,5 @@
---
title: Add rake task to migrate Terraform states to object storage
merge_request: 50740
author:
type: added

View File

@ -100,6 +100,11 @@ See [the available connection settings for different providers](object_storage.m
```
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local states to the object storage (GitLab 13.9 and later):
```shell
gitlab-rake gitlab:terraform_states:migrate
```
**In installations from source:**
@ -120,3 +125,8 @@ See [the available connection settings for different providers](object_storage.m
```
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
1. Migrate any existing local states to the object storage (GitLab 13.9 and later):
```shell
sudo -u git -H bundle exec rake gitlab:terraform_states:migrate RAILS_ENV=production
```

View File

@ -4,5 +4,5 @@ redirect_to: 'paging.md'
This document was moved to [another location](paging.md).
<!-- This redirect file can be deleted after <2022-01-21>. -->
<!-- This redirect file can be deleted after 2021-04-21 -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Gitlab
module Terraform
class StateMigrationHelper
class << self
def migrate_to_remote_storage(&block)
migrate_in_batches(
::Terraform::StateVersion.with_files_stored_locally.preload_state,
::Terraform::StateUploader::Store::REMOTE,
&block
)
end
private
def batch_size
ENV.fetch('MIGRATION_BATCH_SIZE', 10).to_i
end
def migrate_in_batches(versions, store, &block)
versions.find_each(batch_size: batch_size) do |version| # rubocop:disable CodeReuse/ActiveRecord
version.file.migrate!(store)
yield version if block_given?
end
end
end
end
end
end

View File

@ -11,24 +11,12 @@ module Gitlab
@data = data
end
def namespace_id
namespace&.id
end
def project_id
@project&.id
end
def to_context
SnowplowTracker::SelfDescribingJson.new(GITLAB_STANDARD_SCHEMA_URL, to_h)
end
private
def namespace
@namespace || @project&.namespace
end
def to_h
public_methods(false).each_with_object({}) do |method, hash|
next if method == :to_context

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'logger'
desc "GitLab | Terraform | Migrate Terraform states to remote storage"
namespace :gitlab do
namespace :terraform_states do
task migrate: :environment do
logger = Logger.new(STDOUT)
logger.info('Starting transfer of Terraform states to object storage')
begin
Gitlab::Terraform::StateMigrationHelper.migrate_to_remote_storage do |state_version|
message = "Transferred Terraform state version ID #{state_version.id} (#{state_version.terraform_state.name}/#{state_version.version}) to object storage"
logger.info(message)
end
rescue => e
logger.error("Failed to migrate: #{e.message}")
end
end
end
end

View File

@ -30,7 +30,7 @@ module QA
pipeline.visit!
end
it 'runs a Pages-specific pipeline', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/296937' do
it 'runs a Pages-specific pipeline' do
Page::Project::Pipeline::Show.perform do |show|
expect(show).to have_job(:pages)
show.click_job(:pages)

View File

@ -1,4 +1,4 @@
import { textColorForBackground, hexToRgb } from '~/lib/utils/color_utils';
import { textColorForBackground, hexToRgb, validateHexColor } from '~/lib/utils/color_utils';
describe('Color utils', () => {
describe('Converting hex code to rgb', () => {
@ -32,4 +32,19 @@ describe('Color utils', () => {
expect(textColorForBackground('#000')).toEqual('#FFFFFF');
});
});
describe('Validate hex color', () => {
it.each`
color | output
${undefined} | ${null}
${null} | ${null}
${''} | ${null}
${'ABC123'} | ${false}
${'#ZZZ'} | ${false}
${'#FF0'} | ${true}
${'#FF0000'} | ${true}
`('returns $output when $color is given', ({ color, output }) => {
expect(validateHexColor(color)).toEqual(output);
});
});
});

View File

@ -13,6 +13,7 @@ describe('ColorPicker', () => {
};
const setColor = '#000000';
const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
const label = () => wrapper.find(GlFormGroup).attributes('label');
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
@ -55,6 +56,7 @@ describe('ColorPicker', () => {
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
expect(colorInput().props('value')).toBe('');
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
});
it('has a color set on initialization', () => {
@ -67,7 +69,7 @@ describe('ColorPicker', () => {
createComponent();
await colorInput().setValue(setColor);
expect(wrapper.emitted().input[0]).toEqual([setColor]);
expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
@ -75,23 +77,16 @@ describe('ColorPicker', () => {
await colorInput().setValue(` ${setColor} `);
expect(wrapper.vm.$data.selectedColor).toBe(setColor);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
expect(colorInput().attributes('class')).not.toContain('is-invalid');
});
it('shows invalid feedback when an invalid color is used', async () => {
createComponent();
await colorInput().setValue('abcd');
expect(invalidFeedback().text()).toBe(
'Please enter a valid hex (#RRGGBB or #RGB) color value',
);
expect(wrapper.emitted().input).toBe(undefined);
});
it('shows an invalid feedback border on the preview when an invalid color is used', async () => {
createComponent();
await colorInput().setValue('abcd');
it('shows invalid feedback when the state is marked as invalid', async () => {
createComponent(mount, { invalidFeedback: invalidText, state: false });
expect(invalidFeedback().text()).toBe(invalidText);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
expect(colorInput().attributes('class')).toContain('is-invalid');
});
});

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Terraform::StateMigrationHelper do
before do
stub_terraform_state_object_storage
end
describe '.migrate_to_remote_storage' do
let!(:local_version) { create(:terraform_state_version, file_store: Terraform::StateUploader::Store::LOCAL) }
subject { described_class.migrate_to_remote_storage }
it 'migrates remote files to remote storage' do
subject
expect(local_version.reload.file_store).to eq(Terraform::StateUploader::Store::REMOTE)
end
end
end

View File

@ -28,8 +28,8 @@ RSpec.describe Gitlab::Tracking::StandardContext do
context 'with namespace' do
subject { described_class.new(namespace: namespace) }
it 'creates a Snowplow context using the given data' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id)
it 'creates a Snowplow context without namespace and project' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil
expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
end
end
@ -37,18 +37,18 @@ RSpec.describe Gitlab::Tracking::StandardContext do
context 'with project' do
subject { described_class.new(project: project) }
it 'creates a Snowplow context using the given data' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(project.namespace.id)
expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id)
it 'creates a Snowplow context without namespace and project' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil
expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
end
end
context 'with project and namespace' do
subject { described_class.new(namespace: namespace, project: project) }
it 'creates a Snowplow context using the given data' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to eq(namespace.id)
expect(snowplow_context.to_json.dig(:data, :project_id)).to eq(project.id)
it 'creates a Snowplow context without namespace and project' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil
expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
end
end
end

View File

@ -24,6 +24,24 @@ RSpec.describe Terraform::StateVersion do
it { expect(subject.map(&:version)).to eq(versions.sort.reverse) }
end
describe '.with_files_stored_locally' do
subject { described_class.with_files_stored_locally }
it 'includes states with local storage' do
create_list(:terraform_state_version, 5)
expect(subject).to have_attributes(count: 5)
end
it 'excludes states without local storage' do
stub_terraform_state_object_storage
create_list(:terraform_state_version, 5)
expect(subject).to have_attributes(count: 0)
end
end
end
context 'file storage' do

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'rake_helper'
RSpec.describe 'gitlab:terraform_states' do
let_it_be(:version) { create(:terraform_state_version) }
let(:logger) { instance_double(Logger) }
let(:helper) { double }
before(:all) do
Rake.application.rake_require 'tasks/gitlab/terraform/migrate'
end
before do
allow(Logger).to receive(:new).with(STDOUT).and_return(logger)
end
describe 'gitlab:terraform_states:migrate' do
subject { run_rake_task('gitlab:terraform_states:migrate') }
it 'invokes the migration helper to move files to object storage' do
expect(Gitlab::Terraform::StateMigrationHelper).to receive(:migrate_to_remote_storage).and_yield(version)
expect(logger).to receive(:info).with('Starting transfer of Terraform states to object storage')
expect(logger).to receive(:info).with(/Transferred Terraform state version ID #{version.id}/)
subject
end
context 'an error is raised while migrating' do
let(:error_message) { 'Something went wrong' }
before do
allow(Gitlab::Terraform::StateMigrationHelper).to receive(:migrate_to_remote_storage).and_raise(StandardError, error_message)
end
it 'logs the error' do
expect(logger).to receive(:info).with('Starting transfer of Terraform states to object storage')
expect(logger).to receive(:error).with("Failed to migrate: #{error_message}")
subject
end
end
end
end