Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-03-13 21:09:38 +00:00
parent 00fa950a34
commit 232e0a31f1
44 changed files with 1185 additions and 347 deletions

View File

@ -40,7 +40,10 @@ Attach the screenshot and HTML snapshot of the page from the job's artifacts:
/due in 2 weeks
<!-- Base labels. -->
/label ~Quality ~QA ~bug ~S1
/label ~Quality ~QA ~test
<!-- Test failure type label, please use just one.-->
/label ~"failure::broken-test" ~"failure::flaky-test" ~"failure::stale-test" ~"failure::test-environment" ~"failure::investigating"
<!--
Choose the stage that appears in the test path, e.g. ~"devops::create" for

View File

@ -1,57 +1,17 @@
import Vue from 'vue';
import pdfLab from '../../pdf/index.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import PdfViewer from './pdf_viewer.vue';
export default () => {
const el = document.getElementById('js-pdf-viewer');
return new Vue({
el,
components: {
pdfLab,
GlLoadingIcon,
render(createElement) {
return createElement(PdfViewer, {
props: {
pdf: el.dataset.endpoint,
},
});
},
data() {
return {
error: false,
loadError: false,
loading: true,
pdf: el.dataset.endpoint,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
template: `
<div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<gl-loading-icon class="mt-5" size="lg"/>
</div>
<pdf-lab
v-if="!loadError"
:pdf="pdf"
@pdflabload="onLoad"
@pdflaberror="onError" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occurred while loading the file. Please try again later.
</span>
<span v-else>
An error occurred while decoding the file.
</span>
</p>
</div>
`,
});
};

View File

@ -0,0 +1,49 @@
<script>
import PdfLab from '../../pdf/index.vue';
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
PdfLab,
GlLoadingIcon,
},
props: {
pdf: {
type: String,
required: true,
},
},
data() {
return {
error: false,
loadError: false,
loading: true,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
};
</script>
<template>
<div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default">
<div v-if="loading && !error" class="text-center loading">
<gl-loading-icon class="mt-5" size="lg" />
</div>
<pdf-lab v-if="!loadError" :pdf="pdf" @pdflabload="onLoad" @pdflaberror="onError" />
<p v-if="error" class="text-center">
<span v-if="loadError" ref="loadError">
{{ __('An error occurred while loading the file. Please try again later.') }}
</span>
<span v-else>{{ __('An error occurred while decoding the file.') }}</span>
</p>
</div>
</template>

View File

@ -8,6 +8,7 @@ import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { isFunction } from 'lodash';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
@ -667,30 +668,34 @@ export const spriteIcon = (icon, className = '') => {
};
/**
* This method takes in object with snake_case property names
* and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
* @callback ConversionFunction
* @param {string} prop
*/
/**
* This function takes a conversion function as the first parameter
* and applies this function to each prop in the provided object.
*
* This method also supports additional params in `options` object
*
* @param {ConversionFunction} conversionFunction - Function to apply to each prop of the object.
* @param {Object} obj - Object to be converted.
* @param {Object} options - Object containing additional options.
* @param {boolean} options.deep - FLag to allow deep object converting
* @param {Array[]} dropKeys - List of properties to discard while building new object
* @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
* @param {Array[]} options.dropKeys - List of properties to discard while building new object
* @param {Array[]} options.ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
export const convertObjectProps = (conversionFunction, obj = {}, options = {}) => {
if (!isFunction(conversionFunction) || obj === null) {
return {};
}
const initial = Array.isArray(obj) ? [] : {};
const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
const isObjParameterArray = Array.isArray(obj);
const initialValue = isObjParameterArray ? [] : {};
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
const val = obj[prop];
// Drop properties from new object if
@ -702,34 +707,54 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
// Skip converting properties in new object
// if there are any mentioned in options
if (ignoreKeyNames.indexOf(prop) > -1) {
result[prop] = obj[prop];
acc[prop] = val;
return acc;
}
if (deep && (isObject(val) || Array.isArray(val))) {
result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
if (isObjParameterArray) {
acc[prop] = convertObjectProps(conversionFunction, val, options);
} else {
acc[conversionFunction(prop)] = convertObjectProps(conversionFunction, val, options);
}
} else {
result[convertToCamelCase(prop)] = obj[prop];
acc[conversionFunction(prop)] = val;
}
return acc;
}, initial);
}, initialValue);
};
/**
* This method takes in object with snake_case property names
* and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*
* This method also supports additional params in `options` object
*
* @param {Object} obj - Object to be converted.
* @param {Object} options - Object containing additional options.
* @param {boolean} options.deep - FLag to allow deep object converting
* @param {Array[]} options.dropKeys - List of properties to discard while building new object
* @param {Array[]} options.ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) =>
convertObjectProps(convertToCamelCase, obj, options);
/**
* Converts all the object keys to snake case
*
* @param {Object} obj Object to transform
* @returns {Object}
* This method also supports additional params in `options` object
*
* @param {Object} obj - Object to be converted.
* @param {Object} options - Object containing additional options.
* @param {boolean} options.deep - FLag to allow deep object converting
* @param {Array[]} options.dropKeys - List of properties to discard while building new object
* @param {Array[]} options.ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
// Follow up to add additional options param:
// https://gitlab.com/gitlab-org/gitlab/issues/39173
export const convertObjectPropsToSnakeCase = (obj = {}) =>
obj
? Object.entries(obj).reduce(
(acc, [key, value]) => ({ ...acc, [convertToSnakeCase(key)]: value }),
{},
)
: {};
export const convertObjectPropsToSnakeCase = (obj = {}, options = {}) =>
convertObjectProps(convertToSnakeCase, obj, options);
export const imagePath = imgUrl =>
`${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;

View File

@ -7,7 +7,7 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do
push_frontend_feature_flag(:new_variables_ui, @group)
push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true)
end
before_action :define_variables, only: [:show, :create_deploy_token]

View File

@ -21,7 +21,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do
push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true)
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
push_frontend_feature_flag(:single_mr_diff_view, @project)
push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end

View File

@ -6,7 +6,7 @@ module Projects
before_action :authorize_admin_pipeline!
before_action :define_variables
before_action do
push_frontend_feature_flag(:new_variables_ui, @project)
push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true)
end
def show

View File

@ -28,7 +28,8 @@ module Ci
license_scanning: 'gl-license-scanning-report.json',
performance: 'performance.json',
metrics: 'metrics.txt',
lsif: 'lsif.json'
lsif: 'lsif.json',
dotenv: '.env'
}.freeze
INTERNAL_TYPES = {
@ -43,6 +44,7 @@ module Ci
metrics_referee: :gzip,
network_referee: :gzip,
lsif: :gzip,
dotenv: :gzip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
@ -118,7 +120,8 @@ module Ci
metrics: 12, ## EE-specific
metrics_referee: 13, ## runner referees
network_referee: 14, ## runner referees
lsif: 15 # LSIF data for code navigation
lsif: 15, # LSIF data for code navigation
dotenv: 16
}
enum file_format: {

View File

@ -4,11 +4,14 @@ module Ci
class JobVariable < ApplicationRecord
extend Gitlab::Ci::Model
include NewHasVariable
include BulkInsertSafe
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
alias_attribute :secret_value, :value
validates :key, uniqueness: { scope: :job_id }
validates :key, uniqueness: { scope: :job_id }, unless: :dotenv_source?
enum source: { internal: 0, dotenv: 1 }, _suffix: true
end
end

View File

@ -67,14 +67,14 @@ class DiffFileEntity < DiffFileBaseEntity
private
def parallel_diff_view?(options, diff_file)
return true unless Feature.enabled?(:single_mr_diff_view, diff_file.repository.project)
return true unless Feature.enabled?(:single_mr_diff_view, diff_file.repository.project, default_enabled: true)
# If we're not rendering inline, we must be rendering parallel
!inline_diff_view?(options, diff_file)
end
def inline_diff_view?(options, diff_file)
return true unless Feature.enabled?(:single_mr_diff_view, diff_file.repository.project)
return true unless Feature.enabled?(:single_mr_diff_view, diff_file.repository.project, default_enabled: true)
# If nothing is present, inline will be the default.
options.fetch(:diff_view, :inline).to_sym == :inline

View File

@ -10,10 +10,24 @@ module Ci
].freeze
def execute(job, artifacts_file, params, metadata_file: nil)
return success if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file)
artifact, artifact_metadata = build_artifact(job, artifacts_file, params, metadata_file)
result = parse_artifact(job, artifact)
return result unless result[:status] == :success
persist_artifact(job, artifact, artifact_metadata)
end
private
def build_artifact(job, artifacts_file, params, metadata_file)
expire_in = params['expire_in'] ||
Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
job.job_artifacts.build(
artifact = Ci::JobArtifact.new(
job_id: job.id,
project: job.project,
file: artifacts_file,
file_type: params['artifact_type'],
@ -21,34 +35,51 @@ module Ci
file_sha256: artifacts_file.sha256,
expire_in: expire_in)
if metadata_file
job.job_artifacts.build(
project: job.project,
file: metadata_file,
file_type: :metadata,
file_format: :gzip,
file_sha256: metadata_file.sha256,
expire_in: expire_in)
artifact_metadata = if metadata_file
Ci::JobArtifact.new(
job_id: job.id,
project: job.project,
file: metadata_file,
file_type: :metadata,
file_format: :gzip,
file_sha256: metadata_file.sha256,
expire_in: expire_in)
end
[artifact, artifact_metadata]
end
def parse_artifact(job, artifact)
unless Feature.enabled?(:ci_synchronous_artifact_parsing, job.project, default_enabled: true)
return success
end
if job.update(artifacts_expire_in: expire_in)
success
else
error(job.errors.messages, :bad_request)
case artifact.file_type
when 'dotenv' then parse_dotenv_artifact(job, artifact)
else success
end
end
def persist_artifact(job, artifact, artifact_metadata)
Ci::JobArtifact.transaction do
artifact.save!
artifact_metadata&.save!
# NOTE: The `artifacts_expire_at` column is already deprecated and to be removed in the near future.
job.update_column(:artifacts_expire_at, artifact.expire_at)
end
success
rescue ActiveRecord::RecordNotUnique => error
return success if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file)
track_exception(error, job, params)
error('another artifact of the same type already exists', :bad_request)
rescue *OBJECT_STORAGE_ERRORS => error
track_exception(error, job, params)
error(error.message, :service_unavailable)
rescue => error
error(error.message, :bad_request)
end
private
def sha256_matches_existing_artifact?(job, artifact_type, artifacts_file)
existing_artifact = job.job_artifacts.find_by_file_type(artifact_type)
return false unless existing_artifact
@ -63,5 +94,9 @@ module Ci
uploading_type: params['artifact_type']
)
end
def parse_dotenv_artifact(job, artifact)
Ci::ParseDotenvArtifactService.new(job.project, current_user).execute(artifact)
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Ci
class ParseDotenvArtifactService < ::BaseService
MAX_ACCEPTABLE_DOTENV_SIZE = 5.kilobytes
MAX_ACCEPTABLE_VARIABLES_COUNT = 10
SizeLimitError = Class.new(StandardError)
ParserError = Class.new(StandardError)
def execute(artifact)
validate!(artifact)
variables = parse!(artifact)
Ci::JobVariable.bulk_insert!(variables)
success
rescue SizeLimitError, ParserError, ActiveRecord::RecordInvalid => error
Gitlab::ErrorTracking.track_exception(error, job_id: artifact.job_id)
error(error.message, :bad_request)
end
private
def validate!(artifact)
unless artifact&.dotenv?
raise ArgumentError, 'Artifact is not dotenv file type'
end
unless artifact.file.size < MAX_ACCEPTABLE_DOTENV_SIZE
raise SizeLimitError,
"Dotenv Artifact Too Big. Maximum Allowable Size: #{MAX_ACCEPTABLE_DOTENV_SIZE}"
end
end
def parse!(artifact)
variables = []
artifact.each_blob do |blob|
blob.each_line do |line|
key, value = scan_line!(line)
variables << Ci::JobVariable.new(job_id: artifact.job_id,
source: :dotenv, key: key, value: value)
end
end
if variables.size > MAX_ACCEPTABLE_VARIABLES_COUNT
raise SizeLimitError,
"Dotenv files cannot have more than #{MAX_ACCEPTABLE_VARIABLES_COUNT} variables"
end
variables
end
def scan_line!(line)
result = line.scan(/^(.*)=(.*)$/).last
raise ParserError, 'Invalid Format' if result.nil?
result.each(&:strip!)
end
end
end

View File

@ -5,7 +5,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') }
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- if Feature.enabled?(:new_variables_ui, @project || @group)
- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
- is_group = !@group.nil?
#js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} }

View File

@ -0,0 +1,5 @@
---
title: Support DotEnv Variables through report type artifact
merge_request: 26247
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Diffs load each view style separately, on demand
merge_request: 24821
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Update UI for project and group settings CI variables
merge_request: 26901
author:
type: added

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddRuntimeCreatedToCiJobVariables < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
DEFAULT_SOURCE = 0 # Equvalent to Ci::JobVariable.internal_source
def up
add_column_with_default(:ci_job_variables, :source, :integer, limit: 2, default: DEFAULT_SOURCE, allow_null: false)
end
def down
remove_column(:ci_job_variables, :source)
end
end

View File

@ -780,6 +780,7 @@ ActiveRecord::Schema.define(version: 2020_03_12_163407) do
t.string "encrypted_value_iv"
t.bigint "job_id", null: false
t.integer "variable_type", limit: 2, default: 1, null: false
t.integer "source", limit: 2, default: 0, null: false
t.index ["job_id"], name: "index_ci_job_variables_on_job_id"
t.index ["key", "job_id"], name: "index_ci_job_variables_on_key_and_job_id", unique: true
end

View File

@ -156,6 +156,70 @@ Starting with GitLab 9.3, the environment URL is exposed to the Runner via
- `.gitlab-ci.yml`.
- The external URL from the environment if not defined in `.gitlab-ci.yml`.
#### Set dynamic environment URLs after a job finishes
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/17066) in GitLab 12.9.
In a job script, you can specify a static [environment URL](#using-the-environment-url).
However, there may be times when you want a dynamic URL. For example,
if you deploy a Review App to an external hosting
service that generates a random URL per deployment, like `https://94dd65b.amazonaws.com/qa-lambda-1234567`,
you don't know the URL before the deployment script finishes.
If you want to use the environment URL in GitLab, you would have to update it manually.
To address this problem, you can configure a deployment job to report back a set of
variables, including the URL that was dynamically-generated by the external service.
GitLab supports [dotenv](https://github.com/bkeepers/dotenv) file as the format,
and expands the `environment:url` value with variables defined in the dotenv file.
To use this feature, specify the
[`artifacts:reports:dotenv`](yaml/README.md#artifactsreportsdotenv) keyword in `.gitlab-ci.yml`.
##### Example of setting dynamic environment URLs
The following example shows a Review App that creates a new environment
per merge request. The `review` job is triggered by every push, and
creates or updates an environment named `review/your-branch-name`.
The environment URL is set to `$DYNAMIC_ENVIRONMENT_URL`:
```yaml
review:
script:
- DYNAMIC_ENVIRONMENT_URL=$(deploy-script) # In script, get the environment URL.
- echo "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" >> deploy.env # Add the value to a dotenv file.
artifacts:
reports:
dotenv: deploy.env # Report back dotenv file to rails.
environment:
name: review/$CI_COMMIT_REF_SLUG
url: $DYNAMIC_ENVIRONMENT_URL # and set the variable produced in script to `environment:url`
on_stop: stop_review
stop_review:
script:
- ./teardown-environment
when: manual
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
```
As soon as the `review` job finishes, GitLab updates the `review/your-branch-name`
environment's URL.
It parses the report artifact `deploy.env`, registers a list of variables as runtime-created,
uses it for expanding `environment:url: $DYNAMIC_ENVIRONMENT_URL` and sets it to the environment URL.
You can also specify a static part of the URL at `environment:url:`, such as
`https://$DYNAMIC_ENVIRONMENT_URL`. If the value of `DYNAMIC_ENVIRONMENT_URL` is
`123.awesome.com`, the final result will be `https://123.awesome.com`.
The assigned URL for the `review/your-branch-name` environment is visible in the UI.
[See where the environment URL is displayed](#using-the-environment-url).
> **Notes:**
>
> - `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you should not set `environment:url:` in the `stop_review` job.
> - If the environment URL is not valid (for example, the URL is malformed), the system doesn't update the environment URL.
### Configuring manual deployments
Adding `when: manual` to an automatically executed job's configuration converts it to

View File

@ -1,4 +1,5 @@
---
disqus_identifier: 'https://docs.gitlab.com/ee/ci/pipelines.html'
type: reference
---

View File

@ -1,4 +1,5 @@
---
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pipelines/job_artifacts.html'
type: reference, howto
---

View File

@ -1,4 +1,5 @@
---
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pipelines/schedules.html'
type: reference, howto
---

View File

@ -1,4 +1,5 @@
---
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pipelines/settings.html'
type: reference, howto
---

View File

@ -2264,6 +2264,25 @@ concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`),
an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
##### `artifacts:reports:dotenv`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/17066) in GitLab 12.9. Requires GitLab Runner 11.5 and later.
The `dotenv` report collects a set of environment variables as artifacts.
The collected variables are registered as runtime-created variables of the job,
which is useful to [set dynamic environment URLs after a job finishes](../environments.md#set-dynamic-environment-urls-after-a-job-finishes).
It is not available for download through the web interface.
There are a couple of limitations on top of the [original dotenv rules](https://github.com/motdotla/dotenv#rules).
- The variable key can contain only letters, digits and underscore ('_').
- The size of dotenv file must be smaller than 5 kilobytes.
- The number of variables must be less than 10.
- It doesn't support variable substitution in the dotenv file itself.
- It doesn't support empty lines and comments (`#`) in dotenv file.
- It doesn't support quote escape, spaces in a quote, a new line expansion in a quote, in dotenv file.
##### `artifacts:reports:codequality` **(STARTER)**
> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.

View File

@ -39,6 +39,8 @@ To select your issue template for use within Incident Management:
GitLab can react to the alerts that your applications and services may be
triggering by automatically creating issues, and alerting developers via email.
The emails will be sent to [owners and maintainers](../permissions.md) of the project and will contain details on the alert as well as a link to see more information.
### Prometheus alerts
Prometheus alerts can be set up in both:

View File

@ -11,7 +11,10 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics lsif].freeze
ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
dotenv].freeze
attributes ALLOWED_KEYS
@ -31,6 +34,7 @@ module Gitlab
validates :license_scanning, array_of_strings_or_string: true
validates :metrics, array_of_strings_or_string: true
validates :lsif, array_of_strings_or_string: true
validates :dotenv, array_of_strings_or_string: true
end
end

View File

@ -1777,6 +1777,9 @@ msgstr ""
msgid "An error occurred while committing your changes."
msgstr ""
msgid "An error occurred while decoding the file."
msgstr ""
msgid "An error occurred while deleting the approvers group"
msgstr ""
@ -1918,6 +1921,9 @@ msgstr ""
msgid "An error occurred while loading the file."
msgstr ""
msgid "An error occurred while loading the file. Please try again later."
msgstr ""
msgid "An error occurred while loading the merge request changes."
msgstr ""

View File

@ -149,6 +149,16 @@ FactoryBot.define do
end
end
trait :dotenv do
file_type { :dotenv }
file_format { :gzip }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/build.env.gz'), 'application/x-gzip')
end
end
trait :correct_checksum do
after(:build) do |artifact, evaluator|
artifact.file_sha256 = Digest::SHA256.file(artifact.file.path).hexdigest

View File

@ -6,5 +6,9 @@ FactoryBot.define do
value { 'VARIABLE_VALUE' }
job factory: :ci_build
trait :dotenv_source do
source { :dotenv }
end
end
end

BIN
spec/fixtures/build.env.gz vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,67 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { FIXTURES_PATH } from 'spec/test_constants';
import component from '~/blob/pdf/pdf_viewer.vue';
import PdfLab from '~/pdf/index.vue';
const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('PDF renderer', () => {
let wrapper;
const mountComponent = () => {
wrapper = shallowMount(component, {
propsData: {
pdf: testPDF,
},
});
};
const findLoading = () => wrapper.find(GlLoadingIcon);
const findPdfLab = () => wrapper.find(PdfLab);
const findLoadError = () => wrapper.find({ ref: 'loadError' });
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('shows loading icon', () => {
expect(findLoading().exists()).toBe(true);
});
describe('successful response', () => {
beforeEach(() => {
findPdfLab().vm.$emit('pdflabload');
});
it('does not show loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('renders the PDF', () => {
expect(findPdfLab().exists()).toBe(true);
});
});
describe('error getting file', () => {
beforeEach(() => {
findPdfLab().vm.$emit('pdflaberror', 'foo');
});
it('does not show loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('shows error message', () => {
expect(findLoadError().text()).toBe(
'An error occurred while loading the file. Please try again later.',
);
});
});
});

View File

@ -1 +0,0 @@
<div class="file-content" data-endpoint="/test" id="js-pdf-viewer"></div>

View File

@ -539,193 +539,382 @@ describe('common_utils', () => {
});
});
describe('convertObjectPropsToCamelCase', () => {
it('returns new object with camelCase property names by converting object with snake_case names', () => {
const snakeRegEx = /(_\w)/g;
const mockObj = {
id: 1,
group_name: 'GitLab.org',
absolute_web_url: 'https://gitlab.com/gitlab-org/',
};
const mappings = {
id: 'id',
groupName: 'group_name',
absoluteWebUrl: 'absolute_web_url',
};
describe('convertObjectProps*', () => {
const mockConversionFunction = prop => `${prop}_converted`;
const isEmptyObject = obj =>
typeof obj === 'object' && obj !== null && Object.keys(obj).length === 0;
const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj);
const mockObjects = {
convertObjectProps: {
obj: {
id: 1,
group_name: 'GitLab.org',
absolute_web_url: 'https://gitlab.com/gitlab-org/',
},
objNested: {
project_name: 'GitLab CE',
group_name: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
},
convertObjectPropsToCamelCase: {
obj: {
id: 1,
group_name: 'GitLab.org',
absolute_web_url: 'https://gitlab.com/gitlab-org/',
},
objNested: {
project_name: 'GitLab CE',
group_name: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
},
convertObjectPropsToSnakeCase: {
obj: {
id: 1,
groupName: 'GitLab.org',
absoluteWebUrl: 'https://gitlab.com/gitlab-org/',
},
objNested: {
projectName: 'GitLab CE',
groupName: 'GitLab.org',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontendFramework: 'Vue',
database: 'PostgreSQL',
},
},
},
};
Object.keys(convertedObj).forEach(prop => {
expect(snakeRegEx.test(prop)).toBeFalsy();
expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]);
describe('convertObjectProps', () => {
it('returns an empty object if `conversionFunction` parameter is not a function', () => {
const result = commonUtils.convertObjectProps(null, mockObjects.convertObjectProps.obj);
expect(isEmptyObject(result)).toBeTruthy();
});
});
it('return empty object if method is called with null or undefined', () => {
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase(null)).length).toBe(0);
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0);
expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0);
});
describe.each`
functionName | mockObj | mockObjNested
${'convertObjectProps'} | ${mockObjects.convertObjectProps.obj} | ${mockObjects.convertObjectProps.objNested}
${'convertObjectPropsToCamelCase'} | ${mockObjects.convertObjectPropsToCamelCase.obj} | ${mockObjects.convertObjectPropsToCamelCase.objNested}
${'convertObjectPropsToSnakeCase'} | ${mockObjects.convertObjectPropsToSnakeCase.obj} | ${mockObjects.convertObjectPropsToSnakeCase.objNested}
`('$functionName', ({ functionName, mockObj, mockObjNested }) => {
const testFunction =
functionName === 'convertObjectProps'
? (obj, options = {}) =>
commonUtils.convertObjectProps(mockConversionFunction, obj, options)
: commonUtils[functionName];
it('does not deep-convert by default', () => {
const obj = {
snake_key: {
child_snake_key: 'value',
},
};
expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({
snakeKey: {
child_snake_key: 'value',
},
it('returns an empty object if `obj` parameter is null, undefined or an empty object', () => {
expect(isEmptyObject(testFunction(null))).toBeTruthy();
expect(isEmptyObject(testFunction())).toBeTruthy();
expect(isEmptyObject(testFunction({}))).toBeTruthy();
});
});
describe('convertObjectPropsToSnakeCase', () => {
it('converts each object key to snake case', () => {
const obj = {
some: 'some',
'cool object': 'cool object',
likeThisLongOne: 'likeThisLongOne',
it('converts object properties', () => {
const expected = {
convertObjectProps: {
id_converted: 1,
group_name_converted: 'GitLab.org',
absolute_web_url_converted: 'https://gitlab.com/gitlab-org/',
},
convertObjectPropsToCamelCase: {
id: 1,
groupName: 'GitLab.org',
absoluteWebUrl: 'https://gitlab.com/gitlab-org/',
},
convertObjectPropsToSnakeCase: {
id: 1,
group_name: 'GitLab.org',
absolute_web_url: 'https://gitlab.com/gitlab-org/',
},
};
expect(commonUtils.convertObjectPropsToSnakeCase(obj)).toEqual({
some: 'some',
cool_object: 'cool object',
like_this_long_one: 'likeThisLongOne',
});
expect(testFunction(mockObj)).toEqual(expected[functionName]);
});
it('returns an empty object if there are no keys', () => {
['', {}, [], null].forEach(badObj => {
expect(commonUtils.convertObjectPropsToSnakeCase(badObj)).toEqual({});
});
});
});
describe('with options', () => {
const objWithoutChildren = {
project_name: 'GitLab CE',
group_name: 'GitLab.org',
license_type: 'MIT',
};
const objWithChildren = {
project_name: 'GitLab CE',
group_name: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
};
describe('when options.deep is true', () => {
it('converts object with child objects', () => {
const obj = {
snake_key: {
child_snake_key: 'value',
},
};
expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({
snakeKey: {
childSnakeKey: 'value',
},
});
});
it('converts array with child objects', () => {
const arr = [
{
child_snake_key: 'value',
},
];
expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
{
childSnakeKey: 'value',
},
]);
});
it('converts array with child arrays', () => {
const arr = [
[
{
child_snake_key: 'value',
},
],
];
expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
[
{
childSnakeKey: 'value',
},
],
]);
});
});
describe('when options.dropKeys is provided', () => {
it('discards properties mentioned in `dropKeys` array', () => {
expect(
commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, {
dropKeys: ['group_name'],
}),
).toEqual({
projectName: 'GitLab CE',
licenseType: 'MIT',
});
});
it('discards properties mentioned in `dropKeys` array when `deep` is true', () => {
expect(
commonUtils.convertObjectPropsToCamelCase(objWithChildren, {
deep: true,
dropKeys: ['group_name', 'database'],
}),
).toEqual({
projectName: 'GitLab CE',
licenseType: 'MIT',
techStack: {
it('does not deep-convert by default', () => {
const expected = {
convertObjectProps: {
project_name_converted: 'GitLab CE',
group_name_converted: 'GitLab.org',
license_type_converted: 'MIT',
tech_stack_converted: {
backend: 'Ruby',
frontendFramework: 'Vue',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
});
});
});
describe('when options.ignoreKeyNames is provided', () => {
it('leaves properties mentioned in `ignoreKeyNames` array intact', () => {
expect(
commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, {
ignoreKeyNames: ['group_name'],
}),
).toEqual({
},
convertObjectPropsToCamelCase: {
projectName: 'GitLab CE',
licenseType: 'MIT',
group_name: 'GitLab.org',
});
});
it('leaves properties mentioned in `ignoreKeyNames` array intact when `deep` is true', () => {
expect(
commonUtils.convertObjectPropsToCamelCase(objWithChildren, {
deep: true,
ignoreKeyNames: ['group_name', 'frontend_framework'],
}),
).toEqual({
projectName: 'GitLab CE',
group_name: 'GitLab.org',
groupName: 'GitLab.org',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToSnakeCase: {
project_name: 'GitLab CE',
group_name: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontendFramework: 'Vue',
database: 'PostgreSQL',
},
},
};
expect(testFunction(mockObjNested)).toEqual(expected[functionName]);
});
describe('with options', () => {
describe('when options.deep is true', () => {
const expected = {
convertObjectProps: {
project_name_converted: 'GitLab CE',
group_name_converted: 'GitLab.org',
license_type_converted: 'MIT',
tech_stack_converted: {
backend_converted: 'Ruby',
frontend_framework_converted: 'Vue',
database_converted: 'PostgreSQL',
},
},
convertObjectPropsToCamelCase: {
projectName: 'GitLab CE',
groupName: 'GitLab.org',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontendFramework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToSnakeCase: {
project_name: 'GitLab CE',
group_name: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
};
it('converts nested objects', () => {
expect(testFunction(mockObjNested, { deep: true })).toEqual(expected[functionName]);
});
it('converts array of nested objects', () => {
expect(testFunction([mockObjNested], { deep: true })).toEqual([expected[functionName]]);
});
it('converts array with child arrays', () => {
expect(testFunction([[mockObjNested]], { deep: true })).toEqual([
[expected[functionName]],
]);
});
});
describe('when options.dropKeys is provided', () => {
it('discards properties mentioned in `dropKeys` array', () => {
const expected = {
convertObjectProps: {
project_name_converted: 'GitLab CE',
license_type_converted: 'MIT',
tech_stack_converted: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToCamelCase: {
projectName: 'GitLab CE',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToSnakeCase: {
project_name: 'GitLab CE',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontendFramework: 'Vue',
database: 'PostgreSQL',
},
},
};
const dropKeys = {
convertObjectProps: ['group_name'],
convertObjectPropsToCamelCase: ['group_name'],
convertObjectPropsToSnakeCase: ['groupName'],
};
expect(
testFunction(mockObjNested, {
dropKeys: dropKeys[functionName],
}),
).toEqual(expected[functionName]);
});
it('discards properties mentioned in `dropKeys` array when `deep` is true', () => {
const expected = {
convertObjectProps: {
project_name_converted: 'GitLab CE',
license_type_converted: 'MIT',
tech_stack_converted: {
backend_converted: 'Ruby',
frontend_framework_converted: 'Vue',
},
},
convertObjectPropsToCamelCase: {
projectName: 'GitLab CE',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontendFramework: 'Vue',
},
},
convertObjectPropsToSnakeCase: {
project_name: 'GitLab CE',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontend_framework: 'Vue',
},
},
};
const dropKeys = {
convertObjectProps: ['group_name', 'database'],
convertObjectPropsToCamelCase: ['group_name', 'database'],
convertObjectPropsToSnakeCase: ['groupName', 'database'],
};
expect(
testFunction(mockObjNested, {
dropKeys: dropKeys[functionName],
deep: true,
}),
).toEqual(expected[functionName]);
});
});
describe('when options.ignoreKeyNames is provided', () => {
it('leaves properties mentioned in `ignoreKeyNames` array intact', () => {
const expected = {
convertObjectProps: {
project_name_converted: 'GitLab CE',
group_name: 'GitLab.org',
license_type_converted: 'MIT',
tech_stack_converted: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToCamelCase: {
projectName: 'GitLab CE',
group_name: 'GitLab.org',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToSnakeCase: {
project_name: 'GitLab CE',
groupName: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontendFramework: 'Vue',
database: 'PostgreSQL',
},
},
};
const ignoreKeyNames = {
convertObjectProps: ['group_name'],
convertObjectPropsToCamelCase: ['group_name'],
convertObjectPropsToSnakeCase: ['groupName'],
};
expect(
testFunction(mockObjNested, {
ignoreKeyNames: ignoreKeyNames[functionName],
}),
).toEqual(expected[functionName]);
});
it('leaves properties mentioned in `ignoreKeyNames` array intact when `deep` is true', () => {
const expected = {
convertObjectProps: {
project_name_converted: 'GitLab CE',
group_name: 'GitLab.org',
license_type_converted: 'MIT',
tech_stack_converted: {
backend_converted: 'Ruby',
frontend_framework: 'Vue',
database_converted: 'PostgreSQL',
},
},
convertObjectPropsToCamelCase: {
projectName: 'GitLab CE',
group_name: 'GitLab.org',
licenseType: 'MIT',
techStack: {
backend: 'Ruby',
frontend_framework: 'Vue',
database: 'PostgreSQL',
},
},
convertObjectPropsToSnakeCase: {
project_name: 'GitLab CE',
groupName: 'GitLab.org',
license_type: 'MIT',
tech_stack: {
backend: 'Ruby',
frontendFramework: 'Vue',
database: 'PostgreSQL',
},
},
};
const ignoreKeyNames = {
convertObjectProps: ['group_name', 'frontend_framework'],
convertObjectPropsToCamelCase: ['group_name', 'frontend_framework'],
convertObjectPropsToSnakeCase: ['groupName', 'frontendFramework'],
};
expect(
testFunction(mockObjNested, {
deep: true,
ignoreKeyNames: ignoreKeyNames[functionName],
}),
).toEqual(expected[functionName]);
});
});
});

View File

@ -1,72 +0,0 @@
import { FIXTURES_PATH } from 'spec/test_constants';
import renderPDF from '~/blob/pdf';
const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('PDF renderer', () => {
let viewer;
let app;
const checkLoaded = done => {
if (app.loading) {
setTimeout(() => {
checkLoaded(done);
}, 100);
} else {
done();
}
};
preloadFixtures('static/pdf_viewer.html');
beforeEach(() => {
loadFixtures('static/pdf_viewer.html');
viewer = document.getElementById('js-pdf-viewer');
viewer.dataset.endpoint = testPDF;
});
it('shows loading icon', () => {
renderPDF();
expect(document.querySelector('.loading')).not.toBeNull();
});
describe('successful response', () => {
beforeEach(done => {
app = renderPDF();
checkLoaded(done);
});
it('does not show loading icon', () => {
expect(document.querySelector('.loading')).toBeNull();
});
it('renders the PDF', () => {
expect(document.querySelector('.pdf-viewer')).not.toBeNull();
});
it('renders the PDF page', () => {
expect(document.querySelector('.pdf-page')).not.toBeNull();
});
});
describe('error getting file', () => {
beforeEach(done => {
viewer.dataset.endpoint = 'invalid/path/to/file.pdf';
app = renderPDF();
checkLoaded(done);
});
it('does not show loading icon', () => {
expect(document.querySelector('.loading')).toBeNull();
});
it('shows error message', () => {
expect(document.querySelector('.md').textContent.trim()).toBe(
'An error occurred while loading the file. Please try again later.',
);
});
});
});

View File

@ -1,13 +1,10 @@
import Vue from 'vue';
import { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf';
import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
import { FIXTURES_PATH } from 'spec/test_constants';
import PDFLab from '~/pdf/index.vue';
const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
GlobalWorkerOptions.workerSrc = workerSrc;
const Component = Vue.extend(PDFLab);
describe('PDF component', () => {

View File

@ -1,6 +1,5 @@
import Vue from 'vue';
import pdfjsLib from 'pdfjs-dist/build/pdf';
import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
import pdfjsLib from 'pdfjs-dist/webpack';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { FIXTURES_PATH } from 'spec/test_constants';
@ -14,7 +13,6 @@ describe('Page component', () => {
let testPage;
beforeEach(done => {
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
pdfjsLib
.getDocument(testPDF)
.promise.then(pdf => pdf.getPage(1))

View File

@ -44,6 +44,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:license_scanning | 'gl-license-scanning-report.json'
:performance | 'performance.json'
:lsif | 'lsif.json'
:dotenv | 'build.dotenv'
end
with_them do

View File

@ -7,6 +7,11 @@ describe PagesDomain do
subject(:pages_domain) { described_class.new }
# Locking in date due to cert expiration date https://gitlab.com/gitlab-org/gitlab/-/issues/210557#note_304749257
around do |example|
Timecop.travel(Time.new(2020, 3, 12)) { example.run }
end
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:serverless_domain_clusters) }

View File

@ -1937,6 +1937,49 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
end
context 'when artifact_type is dotenv' do
context 'when artifact_format is gzip' do
let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') }
let(:params) { { artifact_type: :dotenv, artifact_format: :gzip } }
it 'stores dotenv file' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_dotenv).not_to be_nil
end
it 'parses dotenv file' do
expect do
upload_artifacts(file_upload, headers_with_token, params)
end.to change { job.job_variables.count }.from(0).to(2)
end
context 'when parse error happens' do
let(:file_upload) { fixture_file_upload('spec/fixtures/ci_build_artifacts_metadata.gz') }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Invalid Format')
end
end
end
context 'when artifact_format is raw' do
let(:file_upload) { fixture_file_upload('spec/fixtures/build.env.gz') }
let(:params) { { artifact_type: :dotenv, artifact_format: :raw } }
it 'returns an error' do
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:bad_request)
expect(job.reload.job_artifacts_dotenv).to be_nil
end
end
end
end
context 'when artifacts already exist for the job' do

View File

@ -121,6 +121,42 @@ describe Ci::CreateJobArtifactsService do
end
end
context 'when artifact type is dotenv' do
let(:artifacts_file) do
file_to_upload('spec/fixtures/build.env.gz', sha256: artifacts_sha256)
end
let(:params) do
{
'artifact_type' => 'dotenv',
'artifact_format' => 'gzip'
}
end
it 'calls parse service' do
expect_any_instance_of(Ci::ParseDotenvArtifactService) do |service|
expect(service).to receive(:execute).once.and_call_original
end
expect(subject[:status]).to eq(:success)
expect(job.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1', 'source' => 'dotenv'),
hash_including('key' => 'KEY2', 'value' => 'VAR2', 'source' => 'dotenv'))
end
context 'when ci_synchronous_artifact_parsing feature flag is disabled' do
before do
stub_feature_flags(ci_synchronous_artifact_parsing: false)
end
it 'does not call parse service' do
expect(Ci::ParseDotenvArtifactService).not_to receive(:new)
expect(subject[:status]).to eq(:success)
end
end
end
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|

View File

@ -0,0 +1,260 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ci::ParseDotenvArtifactService do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline, project: project) }
let(:service) { described_class.new(project, nil) }
describe '#execute' do
subject { service.execute(artifact) }
context 'when build has a dotenv artifact' do
let!(:artifact) { create(:ci_job_artifact, :dotenv, job: build) }
it 'parses the artifact' do
expect(subject[:status]).to eq(:success)
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1'),
hash_including('key' => 'KEY2', 'value' => 'VAR2'))
end
context 'when parse error happens' do
before do
allow(service).to receive(:scan_line!) { raise described_class::ParserError.new('Invalid Format') }
end
it 'returns error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(described_class::ParserError, job_id: build.id)
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Invalid Format')
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when artifact size is too big' do
before do
allow(artifact.file).to receive(:size) { 10.kilobytes }
end
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq("Dotenv Artifact Too Big. Maximum Allowable Size: #{described_class::MAX_ACCEPTABLE_DOTENV_SIZE}")
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when artifact has the specified blob' do
before do
allow(artifact).to receive(:each_blob).and_yield(blob)
end
context 'when a white space trails the key' do
let(:blob) { 'KEY1 =VAR1' }
it 'trims the trailing space' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1'))
end
end
context 'when multiple key/value pairs exist in one line' do
let(:blob) { 'KEY1=VAR1KEY2=VAR1' }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.")
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when key contains UNICODE' do
let(:blob) { '🛹=skateboard' }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.")
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when value contains UNICODE' do
let(:blob) { 'skateboard=🛹' }
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'skateboard', 'value' => '🛹'))
end
end
context 'when key contains a space' do
let(:blob) { 'K E Y 1=VAR1' }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.")
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when value contains a space' do
let(:blob) { 'KEY1=V A R 1' }
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'V A R 1'))
end
end
context 'when value is double quoated' do
let(:blob) { 'KEY1="VAR1"' }
it 'parses the value as-is' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => '"VAR1"'))
end
end
context 'when value is single quoated' do
let(:blob) { "KEY1='VAR1'" }
it 'parses the value as-is' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => "'VAR1'"))
end
end
context 'when value has white spaces in double quote' do
let(:blob) { 'KEY1=" VAR1 "' }
it 'parses the value as-is' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => '" VAR1 "'))
end
end
context 'when key is missing' do
let(:blob) { '=VAR1' }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to match(/Key can't be blank/)
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when value is missing' do
let(:blob) { 'KEY1=' }
it 'parses the dotenv data' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => ''))
end
end
context 'when it is not dotenv format' do
let(:blob) { "{ 'KEY1': 'VAR1' }" }
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Invalid Format')
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when more than limitated variables are specified in dotenv' do
let(:blob) do
StringIO.new.tap do |s|
(described_class::MAX_ACCEPTABLE_VARIABLES_COUNT + 1).times do |i|
s << "KEY#{i}=VAR#{i}\n"
end
end.string
end
it 'returns error' do
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq("Dotenv files cannot have more than #{described_class::MAX_ACCEPTABLE_VARIABLES_COUNT} variables")
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when variables are cross-referenced in dotenv' do
let(:blob) do
<<~EOS
KEY1=VAR1
KEY2=${KEY1}_Test
EOS
end
it 'does not support variable expansion in dotenv parser' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1'),
hash_including('key' => 'KEY2', 'value' => '${KEY1}_Test'))
end
end
context 'when there is an empty line' do
let(:blob) do
<<~EOS
KEY1=VAR1
KEY2=VAR2
EOS
end
it 'does not support empty line in dotenv parser' do
subject
expect(subject[:status]).to eq(:error)
expect(subject[:message]).to eq('Invalid Format')
expect(subject[:http_status]).to eq(:bad_request)
end
end
context 'when there is a comment' do
let(:blob) do
<<~EOS
KEY1=VAR1 # This is variable
EOS
end
it 'does not support comment in dotenv parser' do
subject
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'KEY1', 'value' => 'VAR1 # This is variable'))
end
end
end
end
context 'when build does not have a dotenv artifact' do
let!(:artifact) { }
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError)
end
end
end
end

View File

@ -36,7 +36,7 @@ describe Ci::RetryBuildService do
job_artifacts_performance job_artifacts_lsif
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee needs].freeze
job_artifacts_network_referee job_artifacts_dotenv needs].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections

View File

@ -177,6 +177,26 @@ describe Deployments::AfterCreateService do
it { is_expected.to eq('http://review/host') }
end
context 'when job variables are generated during runtime' do
let(:job) do
create(:ci_build,
:with_deployment,
pipeline: pipeline,
environment: 'review/$CI_COMMIT_REF_NAME',
project: project,
job_variables: [job_variable],
options: { environment: { name: 'review/$CI_COMMIT_REF_NAME', url: 'http://$DYNAMIC_ENV_URL' } })
end
let(:job_variable) do
build(:ci_job_variable, :dotenv_source, key: 'DYNAMIC_ENV_URL', value: 'abc.test.com')
end
it 'expands the environment URL from the dynamic variable' do
is_expected.to eq('http://abc.test.com')
end
end
context 'when yaml environment does not have url' do
let(:job) { create(:ci_build, :with_deployment, pipeline: pipeline, environment: 'staging', project: project) }

View File

@ -7,6 +7,11 @@ describe PagesDomainSslRenewalCronWorker do
subject(:worker) { described_class.new }
# Locking in date due to cert expiration date https://gitlab.com/gitlab-org/gitlab/-/issues/210557#note_304749257
around do |example|
Timecop.travel(Time.new(2020, 3, 12)) { example.run }
end
before do
stub_lets_encrypt_settings
end