Commit 4eb8816d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 87d95ab0 0c902636
40fae4205d3ad62ca9341620146486bee8d31b28 d924490032231edb9452acdaca7d8e4747cf6ab4
...@@ -22,10 +22,6 @@ export default { ...@@ -22,10 +22,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
canReadEnvironment: {
type: Boolean,
required: true,
},
}, },
methods: { methods: {
onChangePage(page) { onChangePage(page) {
...@@ -42,7 +38,7 @@ export default { ...@@ -42,7 +38,7 @@ export default {
<slot name="empty-state"></slot> <slot name="empty-state"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder"> <div v-if="!isLoading && environments.length > 0" class="table-holder">
<environment-table :environments="environments" :can-read-environment="canReadEnvironment" /> <environment-table :environments="environments" />
<table-pagination <table-pagination
v-if="pagination && pagination.totalPages > 1" v-if="pagination && pagination.totalPages > 1"
......
...@@ -48,12 +48,6 @@ export default { ...@@ -48,12 +48,6 @@ export default {
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
model: { model: {
type: Object, type: Object,
required: true, required: true,
...@@ -790,14 +784,14 @@ export default { ...@@ -790,14 +784,14 @@ export default {
/> />
<external-url-component <external-url-component
v-if="externalURL && canReadEnvironment" v-if="externalURL"
:external-url="externalURL" :external-url="externalURL"
data-track-action="click_button" data-track-action="click_button"
data-track-label="environment_url" data-track-label="environment_url"
/> />
<monitoring-button-component <monitoring-button-component
v-if="monitoringUrl && canReadEnvironment" v-if="monitoringUrl"
:monitoring-url="monitoringUrl" :monitoring-url="monitoringUrl"
data-track-action="click_button" data-track-action="click_button"
data-track-label="environment_monitoring" data-track-label="environment_monitoring"
......
...@@ -52,10 +52,6 @@ export default { ...@@ -52,10 +52,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
canReadEnvironment: {
type: Boolean,
required: true,
},
newEnvironmentPath: { newEnvironmentPath: {
type: String, type: String,
required: true, required: true,
...@@ -210,7 +206,6 @@ export default { ...@@ -210,7 +206,6 @@ export default {
:is-loading="isLoading" :is-loading="isLoading"
:environments="state.environments" :environments="state.environments"
:pagination="state.paginationInformation" :pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage" @onChangePage="onChangePage"
> >
<template v-if="!isLoading && state.environments.length === 0" #empty-state> <template v-if="!isLoading && state.environments.length === 0" #empty-state>
......
...@@ -27,10 +27,6 @@ export default { ...@@ -27,10 +27,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
canReadEnvironment: {
type: Boolean,
required: true,
},
canAdminEnvironment: { canAdminEnvironment: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -84,7 +80,7 @@ export default { ...@@ -84,7 +80,7 @@ export default {
return this.environment.isAvailable && Boolean(this.environment.autoStopAt); return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
}, },
shouldShowExternalUrlButton() { shouldShowExternalUrlButton() {
return this.canReadEnvironment && Boolean(this.environment.externalUrl); return Boolean(this.environment.externalUrl);
}, },
shouldShowStopButton() { shouldShowStopButton() {
return this.canStopEnvironment && this.environment.isAvailable; return this.canStopEnvironment && this.environment.isAvailable;
...@@ -138,7 +134,7 @@ export default { ...@@ -138,7 +134,7 @@ export default {
>{{ $options.i18n.externalButtonText }}</gl-button >{{ $options.i18n.externalButtonText }}</gl-button
> >
<gl-button <gl-button
v-if="canReadEnvironment" v-if="shouldShowExternalUrlButton"
data-testid="metrics-button" data-testid="metrics-button"
:href="metricsPath" :href="metricsPath"
:title="$options.i18n.metricsButtonTitle" :title="$options.i18n.metricsButtonTitle"
......
...@@ -23,11 +23,6 @@ export default { ...@@ -23,11 +23,6 @@ export default {
required: true, required: true,
default: () => [], default: () => [],
}, },
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -155,7 +150,6 @@ export default { ...@@ -155,7 +150,6 @@ export default {
<environment-item <environment-item
:key="`environment-item-${i}`" :key="`environment-item-${i}`"
:model="model" :model="model"
:can-read-environment="canReadEnvironment"
:table-data="tableData" :table-data="tableData"
data-qa-selector="environment_item" data-qa-selector="environment_item"
/> />
...@@ -191,7 +185,6 @@ export default { ...@@ -191,7 +185,6 @@ export default {
<environment-item <environment-item
:key="`environment-row-${i}-${index}`" :key="`environment-row-${i}-${index}`"
:model="child" :model="child"
:can-read-environment="canReadEnvironment"
:table-data="tableData" :table-data="tableData"
data-qa-selector="environment_item" data-qa-selector="environment_item"
/> />
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate'; import Translate from '../../vue_shared/translate';
import environmentsFolderApp from './environments_folder_view.vue'; import environmentsFolderApp from './environments_folder_view.vue';
...@@ -31,7 +30,6 @@ export default () => { ...@@ -31,7 +30,6 @@ export default () => {
endpoint: environmentsData.environmentsDataEndpoint, endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName, folderName: environmentsData.environmentsDataFolderName,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
}; };
}, },
render(createElement) { render(createElement) {
...@@ -40,7 +38,6 @@ export default () => { ...@@ -40,7 +38,6 @@ export default () => {
endpoint: this.endpoint, endpoint: this.endpoint,
folderName: this.folderName, folderName: this.folderName,
cssContainerClass: this.cssContainerClass, cssContainerClass: this.cssContainerClass,
canReadEnvironment: this.canReadEnvironment,
}, },
}); });
}, },
......
...@@ -30,10 +30,6 @@ export default { ...@@ -30,10 +30,6 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
canReadEnvironment: {
type: Boolean,
required: true,
},
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
...@@ -72,7 +68,6 @@ export default { ...@@ -72,7 +68,6 @@ export default {
:is-loading="isLoading" :is-loading="isLoading"
:environments="state.environments" :environments="state.environments"
:pagination="state.paginationInformation" :pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage" @onChangePage="onChangePage"
/> />
</div> </div>
......
...@@ -32,7 +32,6 @@ export default () => { ...@@ -32,7 +32,6 @@ export default () => {
newEnvironmentPath: environmentsData.newEnvironmentPath, newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath, helpPagePath: environmentsData.helpPagePath,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment), canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
}; };
}, },
render(createElement) { render(createElement) {
...@@ -42,7 +41,6 @@ export default () => { ...@@ -42,7 +41,6 @@ export default () => {
newEnvironmentPath: this.newEnvironmentPath, newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath, helpPagePath: this.helpPagePath,
canCreateEnvironment: this.canCreateEnvironment, canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment,
}, },
}); });
}, },
......
...@@ -36,7 +36,6 @@ export const initHeader = () => { ...@@ -36,7 +36,6 @@ export const initHeader = () => {
environment: this.environment, environment: this.environment,
canDestroyEnvironment: dataset.canDestroyEnvironment, canDestroyEnvironment: dataset.canDestroyEnvironment,
canUpdateEnvironment: dataset.canUpdateEnvironment, canUpdateEnvironment: dataset.canUpdateEnvironment,
canReadEnvironment: dataset.canReadEnvironment,
canStopEnvironment: dataset.canStopEnvironment, canStopEnvironment: dataset.canStopEnvironment,
canAdminEnvironment: dataset.canAdminEnvironment, canAdminEnvironment: dataset.canAdminEnvironment,
cancelAutoStopPath: dataset.environmentCancelAutoStopPath, cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
......
<script>
export default {
props: {
url: {
type: String,
required: true,
},
alt: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="gl-text-center gl-p-7 gl-bg-gray-50">
<img :src="url" :alt="alt" data-testid="image" />
</div>
</template>
...@@ -6,6 +6,8 @@ export const loadViewer = (type) => { ...@@ -6,6 +6,8 @@ export const loadViewer = (type) => {
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue'); return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
case 'download': case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue'); return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
case 'image':
return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
default: default:
return null; return null;
} }
...@@ -23,5 +25,9 @@ export const viewerProps = (type, blob) => { ...@@ -23,5 +25,9 @@ export const viewerProps = (type, blob) => {
filePath: blob.rawPath, filePath: blob.rawPath,
fileSize: blob.rawSize, fileSize: blob.rawSize,
}, },
image: {
url: blob.rawPath,
alt: blob.name,
},
}[type]; }[type];
}; };
...@@ -2,7 +2,7 @@ export default (search = '') => { ...@@ -2,7 +2,7 @@ export default (search = '') => {
const highlightLineClass = 'hll'; const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body'); const contentBody = document.getElementById('content-body');
const searchTerm = search.toLowerCase(); const searchTerm = search.toLowerCase();
const blobs = contentBody.querySelectorAll('.blob-result'); const blobs = contentBody.querySelectorAll('.js-blob-result');
blobs.forEach((blob) => { blobs.forEach((blob) => {
const lines = blob.querySelectorAll('.line'); const lines = blob.querySelectorAll('.line');
......
...@@ -73,7 +73,6 @@ module EnvironmentHelper ...@@ -73,7 +73,6 @@ module EnvironmentHelper
external_url: environment.external_url, external_url: environment.external_url,
can_update_environment: can?(current_user, :update_environment, environment), can_update_environment: can?(current_user, :update_environment, environment),
can_destroy_environment: can_destroy_environment?(environment), can_destroy_environment: can_destroy_environment?(environment),
can_read_environment: can?(current_user, :read_environment, environment),
can_stop_environment: can?(current_user, :stop_environment, environment), can_stop_environment: can?(current_user, :stop_environment, environment),
can_admin_environment: can?(current_user, :admin_environment, project), can_admin_environment: can?(current_user, :admin_environment, project),
environment_metrics_path: environment_metrics_path(environment), environment_metrics_path: environment_metrics_path(environment),
......
...@@ -14,12 +14,10 @@ module CronSchedulable ...@@ -14,12 +14,10 @@ module CronSchedulable
# The `next_run_at` column is set to the actual execution date of worker that # The `next_run_at` column is set to the actual execution date of worker that
# triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered # triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
# in a short interval when the worker runs irregularly by Sidekiq Memory Killer. # in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
def calculate_next_run_at def calculate_next_run_at(start_time = Time.zone.now)
now = Time.zone.now ideal_next_run = ideal_next_run_from(start_time)
ideal_next_run = ideal_next_run_from(now) if ideal_next_run == cron_worker_next_run_from(start_time)
if ideal_next_run == cron_worker_next_run_from(now)
ideal_next_run ideal_next_run
else else
cron_worker_next_run_from(ideal_next_run) cron_worker_next_run_from(ideal_next_run)
......
...@@ -12,11 +12,11 @@ module Ci ...@@ -12,11 +12,11 @@ module Ci
erased_by.name if erased_by_user? erased_by.name if erased_by_user?
end end
def status_title def status_title(status = detailed_status)
if auto_canceled? if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
else else
tooltip_for_badge tooltip_for_badge(status)
end end
end end
...@@ -41,8 +41,8 @@ module Ci ...@@ -41,8 +41,8 @@ module Ci
private private
def tooltip_for_badge def tooltip_for_badge(status)
detailed_status.badge_tooltip.capitalize status.badge_tooltip.capitalize
end end
def detailed_status def detailed_status
......
...@@ -15,18 +15,23 @@ module Ci ...@@ -15,18 +15,23 @@ module Ci
private private
def preload_statuses(statuses) def preload_statuses(statuses)
loaded_statuses = statuses.load common_relations = [:pipeline]
statuses.tap do |statuses|
# rubocop: disable CodeReuse/ActiveRecord
ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(loaded_statuses), %w[pipeline tags job_artifacts_archive metadata])
# rubocop: enable CodeReuse/ActiveRecord
end
end
def preloadable_statuses(statuses) preloaders = {
statuses.reject do |status| ::Ci::Build => [:metadata, :tags, :job_artifacts_archive],
status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge) ::Ci::Bridge => [:metadata, :downstream_pipeline],
::GenericCommitStatus => []
}
# rubocop: disable CodeReuse/ActiveRecord
preloaders.each do |klass, relations|
ActiveRecord::Associations::Preloader
.new
.preload(statuses.select { |job| job.is_a?(klass) }, relations + common_relations)
end end
# rubocop: enable CodeReuse/ActiveRecord
statuses
end end
end end
end end
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Dast profile schedule cadence schema",
"type": "object",
"anyOf": [
{
"properties": {
"unit": { "enum": ["day"] },
"duration": { "enum": [1] }
}
},
{
"properties": {
"unit": { "enum": ["week"] },
"duration": { "enum": [1] }
}
},
{
"properties": {
"unit": { "enum": ["month"] },
"duration": { "enum": [1, 3 ,6] }
}
},
{
"properties": {
"unit": { "enum": ["year"] },
"duration": { "enum": [1] }
}
}
]
}
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
.float-right .float-right
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f| = form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
= f.hidden_field :runner_id, value: @runner.id = f.hidden_field :runner_id, value: @runner.id
= f.submit _('Enable'), class: 'gl-button btn btn-sm' = f.submit _('Enable'), class: 'gl-button btn btn-sm', data: { confirm: (s_('Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?') if @runner.instance_type?) }
= paginate_without_count @projects = paginate_without_count @projects
.col-md-6 .col-md-6
......
...@@ -7,10 +7,14 @@ ...@@ -7,10 +7,14 @@
- pipeline_link = local_assigns.fetch(:pipeline_link, false) - pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false) - stage = local_assigns.fetch(:stage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false) - allow_retry = local_assigns.fetch(:allow_retry, false)
-# This prevents initializing another Ci::Status object where 'status' is used
- status = job.detailed_status(current_user)
%tr.build.commit{ class: ('retried' if retried) } %tr.build.commit{ class: ('retried' if retried) }
%td.status %td.status
= render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title -# Sending 'status' prevents calling the user relation inside the presenter, generating N+1,
-# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68743
= render "ci/status/badge", status: status, title: job.status_title(status)
%td %td
- if can?(current_user, :read_build, job) - if can?(current_user, :read_build, job)
......
.blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } } .js-blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
.file-holder.file-holder-top-border .file-holder.file-holder-top-border
.js-file-title.file-title{ data: { qa_selector: 'file_title_content' } } .js-file-title.file-title{ data: { qa_selector: 'file_title_content' } }
= link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do = link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do
......
---
name: method_instrumentation_disable_initialization
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69091
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339665
milestone: '14.3'
type: development
group: group::memory
default_enabled: false
...@@ -178,27 +178,33 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d ...@@ -178,27 +178,33 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s) ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
) )
Gitlab::Metrics::Instrumentation # We are removing the Instrumentation module entirely in steps.
.instrument_class_hierarchy(ActiveRecord::Base) do |klass, method| # More in https://gitlab.com/gitlab-org/gitlab/-/issues/217978.
# Instrumenting the ApplicationSetting class can lead to an infinite unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
# loop. Since the data is cached any way we don't really need to Gitlab::Metrics::Instrumentation
# instrument it. .instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
if klass == ApplicationSetting # Instrumenting the ApplicationSetting class can lead to an infinite
false # loop. Since the data is cached any way we don't really need to
else # instrument it.
loc = method.source_location if klass == ApplicationSetting
false
loc && loc[0].start_with?(models_path) && method.source =~ regex else
loc = method.source_location
loc && loc[0].start_with?(models_path) && method.source =~ regex
end
end end
end
# Ability is in app/models, is not an ActiveRecord model, but should still # Ability is in app/models, is not an ActiveRecord model, but should still
# be instrumented. # be instrumented.
Gitlab::Metrics::Instrumentation.instrument_methods(Ability) Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
end
end end
Gitlab::Metrics::Instrumentation.configure do |config| unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
instrument_classes(config) Gitlab::Metrics::Instrumentation.configure do |config|
instrument_classes(config)
end
end end
GC::Profiler.enable GC::Profiler.enable
......
# frozen_string_literal: true
class AddCadenceToDastProfileSchedules < ActiveRecord::Migration[6.1]
def change
add_column :dast_profile_schedules, :cadence, :jsonb, null: false, default: {}
end
end
# frozen_string_literal: true
class AddTimezoneToDastProfileSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# We disable these cops here because adding the column is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Rails/NotNullColumn
def up
execute('DELETE FROM dast_profile_schedules')
unless column_exists?(:dast_profile_schedules, :timezone)
add_column :dast_profile_schedules, :timezone, :text, null: false
end
add_text_limit :dast_profile_schedules, :timezone, 255
end
def down
return unless column_exists?(:dast_profile_schedules, :timezone)
remove_column :dast_profile_schedules, :timezone
end
end
# frozen_string_literal: true
class AddStartsAtToDastProfileSchedules < ActiveRecord::Migration[6.1]
def change
add_column :dast_profile_schedules, :starts_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddUniqueIndexOnDastProfileToDastProfileSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_dast_profile_schedules_on_dast_profile_id'
TABLE = :dast_profile_schedules
# We disable these cops here because changing this index is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Migration/AddIndex
# rubocop: disable Migration/RemoveIndex
def up
execute('DELETE FROM dast_profile_schedules')
if index_exists_by_name?(TABLE, INDEX_NAME)
remove_index TABLE, :dast_profile_id, name: INDEX_NAME
end
unless index_exists_by_name?(TABLE, INDEX_NAME)
add_index TABLE, :dast_profile_id, unique: true, name: INDEX_NAME
end
end
def down
execute('DELETE FROM dast_profile_schedules')
if index_exists_by_name?(TABLE, INDEX_NAME)
remove_index TABLE, :dast_profile_id, name: INDEX_NAME
end
unless index_exists_by_name?(TABLE, INDEX_NAME)
add_index TABLE, :dast_profile_id
end
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectProfileCompoundIndexFromDastProfileSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
TABLE = :dast_profile_schedules
INDEX_NAME = 'index_dast_profile_schedules_on_project_id_and_dast_profile_id'
# We disable these cops here because changing this index is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Migration/AddIndex
# rubocop: disable Migration/RemoveIndex
def up
execute('DELETE FROM dast_profile_schedules')
if index_exists_by_name?(TABLE, INDEX_NAME)
remove_index TABLE, %i[project_id dast_profile_id], name: INDEX_NAME
end
end
def down
execute('DELETE FROM dast_profile_schedules')
unless index_exists_by_name?(TABLE, INDEX_NAME)
add_index TABLE, %i[project_id dast_profile_id], unique: true, name: INDEX_NAME
end
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexProjectIdOnDastProfileSchedule < ActiveRecord::Migration[6.1]
# We disable these cops here because changing this index is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Migration/AddIndex
def change
add_index :dast_profile_schedules, :project_id
end
end
30e1463616c60b92afb28bbb76e3c55830a385af6df0e60e16ed96d9e75943b9
\ No newline at end of file
7e9b39914ade766357751953a4981225dbae7e5d371d4824af61b01af70f46ae
\ No newline at end of file
a2454f9fca3b1cedf7a0f2288b69abe799fe1f9ff4e2fe26d2cadfdddea73a83
\ No newline at end of file
d1ad234656f49861d2ca7694d23116e930bba597fca32b1015db698cc23bdc1c
\ No newline at end of file
23becdc9ad558882f4ce42e76391cdc2f760322a09c998082465fcb6d29dfeb5
\ No newline at end of file
9c5114dac05e90c15567bb3274f20f03a82f9e4d73d5c72d89c26bc9d742cc35
\ No newline at end of file
...@@ -12089,7 +12089,11 @@ CREATE TABLE dast_profile_schedules ( ...@@ -12089,7 +12089,11 @@ CREATE TABLE dast_profile_schedules (
updated_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL,
active boolean DEFAULT true NOT NULL, active boolean DEFAULT true NOT NULL,
cron text NOT NULL, cron text NOT NULL,
CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255)) cadence jsonb DEFAULT '{}'::jsonb NOT NULL,
timezone text NOT NULL,
starts_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255)),
CONSTRAINT check_be4d1c3af1 CHECK ((char_length(timezone) <= 255))
); );
COMMENT ON TABLE dast_profile_schedules IS '{"owner":"group::dynamic analysis","description":"Scheduling for scans using DAST Profiles"}'; COMMENT ON TABLE dast_profile_schedules IS '{"owner":"group::dynamic analysis","description":"Scheduling for scans using DAST Profiles"}';
...@@ -23812,9 +23816,9 @@ CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_ ...@@ -23812,9 +23816,9 @@ CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_
CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at); CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at);
CREATE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id); CREATE UNIQUE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
CREATE UNIQUE INDEX index_dast_profile_schedules_on_project_id_and_dast_profile_id ON dast_profile_schedules USING btree (project_id, dast_profile_id); CREATE INDEX index_dast_profile_schedules_on_project_id ON dast_profile_schedules USING btree (project_id);
CREATE INDEX index_dast_profile_schedules_on_user_id ON dast_profile_schedules USING btree (user_id); CREATE INDEX index_dast_profile_schedules_on_user_id ON dast_profile_schedules USING btree (user_id);
...@@ -95,7 +95,7 @@ GitLab provides a series of [CI templates that you can include in your project]( ...@@ -95,7 +95,7 @@ GitLab provides a series of [CI templates that you can include in your project](
To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS) To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS)
cluster, you can `include` the `AWS/Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file. cluster, you can `include` the `AWS/Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file.
GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `gitlab-ci.yml` file to simplify working with AWS: GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `.gitlab-ci.yml` file to simplify working with AWS:
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest` to use AWS CLI commands. - Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest` to use AWS CLI commands.
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest` to deploy your application to AWS ECS. - Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest` to deploy your application to AWS ECS.
......
...@@ -136,10 +136,10 @@ connect the CD project to your development projects by using [multi-project pipe ...@@ -136,10 +136,10 @@ connect the CD project to your development projects by using [multi-project pipe
A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This
deployment usually runs automatically after pushing a merge request. To prevent developers from deployment usually runs automatically after pushing a merge request. To prevent developers from
changing the `gitlab-ci.yml`, you can define it in a different repository. The configuration can changing the `.gitlab-ci.yml`, you can define it in a different repository. The configuration can
reference a file in another project with a completely different set of permissions (similar to reference a file in another project with a completely different set of permissions (similar to
[separating a project for deployments](#separate-project-for-deployments)). [separating a project for deployments](#separate-project-for-deployments)).
In this scenario, the `gitlab-ci.yml` is publicly accessible, but can only be edited by users with In this scenario, the `.gitlab-ci.yml` is publicly accessible, but can only be edited by users with
appropriate permissions in the other project. appropriate permissions in the other project.
For more information, see [Custom CI/CD configuration path](../pipelines/settings.md#specify-a-custom-cicd-configuration-file). For more information, see [Custom CI/CD configuration path](../pipelines/settings.md#specify-a-custom-cicd-configuration-file).
......
...@@ -135,5 +135,5 @@ to switch to a different deployment. Both deployments are running in parallel, a ...@@ -135,5 +135,5 @@ to switch to a different deployment. Both deployments are running in parallel, a
can be switched to at any time. can be switched to at any time.
An [example deployable application](https://gitlab.com/gl-release/blue-green-example) An [example deployable application](https://gitlab.com/gl-release/blue-green-example)
is available, with a [`gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml) is available, with a [`.gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
that demonstrates blue-green deployments. that demonstrates blue-green deployments.
...@@ -177,7 +177,7 @@ You can find the play button in the pipelines, environments, deployments, and jo ...@@ -177,7 +177,7 @@ You can find the play button in the pipelines, environments, deployments, and jo
If you are deploying to a [Kubernetes cluster](../../user/project/clusters/index.md) If you are deploying to a [Kubernetes cluster](../../user/project/clusters/index.md)
associated with your project, you can configure these deployments from your associated with your project, you can configure these deployments from your
`gitlab-ci.yml` file. `.gitlab-ci.yml` file.
NOTE: NOTE:
Kubernetes configuration isn't supported for Kubernetes clusters that are Kubernetes configuration isn't supported for Kubernetes clusters that are
......
...@@ -251,7 +251,7 @@ To protect a group-level environment: ...@@ -251,7 +251,7 @@ To protect a group-level environment:
1. Make sure your environments have the correct 1. Make sure your environments have the correct
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in [`deployment_tier`](index.md#deployment-tier-of-environments) defined in
`gitlab-ci.yml`. `.gitlab-ci.yml`.
1. Configure the group-level protected environments via the 1. Configure the group-level protected environments via the
[REST API](../../api/group_protected_environments.md). [REST API](../../api/group_protected_environments.md).
......
...@@ -56,7 +56,7 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint ...@@ -56,7 +56,7 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7. > - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.12. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.12.
To view a visualization of your `gitlab-ci.yml` configuration, in your project, To view a visualization of your `.gitlab-ci.yml` configuration, in your project,
go to **CI/CD > Editor**, and then select the **Visualize** tab. The go to **CI/CD > Editor**, and then select the **Visualize** tab. The
visualization shows all stages and jobs. Any [`needs`](../yaml/index.md#needs) visualization shows all stages and jobs. Any [`needs`](../yaml/index.md#needs)
relationships are displayed as lines connecting jobs together, showing the relationships are displayed as lines connecting jobs together, showing the
......
...@@ -29,7 +29,7 @@ with your editor of choice. ...@@ -29,7 +29,7 @@ with your editor of choice.
### Verify syntax with CI Lint tool ### Verify syntax with CI Lint tool
The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration
file is correct. Paste in full `gitlab-ci.yml` files or individual jobs configuration, file is correct. Paste in full `.gitlab-ci.yml` files or individual jobs configuration,
to verify the basic syntax. to verify the basic syntax.
When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
...@@ -49,7 +49,7 @@ and check if their values are what you expect. ...@@ -49,7 +49,7 @@ and check if their values are what you expect.
## GitLab CI/CD documentation ## GitLab CI/CD documentation
The [complete `gitlab-ci.yml` reference](yaml/index.md) contains a full list of The [complete `.gitlab-ci.yml` reference](yaml/index.md) contains a full list of
every keyword you may need to use to configure your pipelines. every keyword you may need to use to configure your pipelines.
You can also look at a large number of pipeline configuration [examples](examples/index.md) You can also look at a large number of pipeline configuration [examples](examples/index.md)
......
...@@ -386,7 +386,7 @@ does not block triggered pipelines. ...@@ -386,7 +386,7 @@ does not block triggered pipelines.
> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4. > [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4.
Use `include` to include external YAML files in your CI/CD configuration. Use `include` to include external YAML files in your CI/CD configuration.
You can break down one long `gitlab-ci.yml` file into multiple files to increase readability, You can break down one long `.gitlab-ci.yml` file into multiple files to increase readability,
or reduce duplication of the same configuration in multiple places. or reduce duplication of the same configuration in multiple places.
You can also store template files in a central repository and `include` them in projects. You can also store template files in a central repository and `include` them in projects.
...@@ -4483,7 +4483,7 @@ deploy_review_job: ...@@ -4483,7 +4483,7 @@ deploy_review_job:
You can use only integers and strings for the variable's name and value. You can use only integers and strings for the variable's name and value.
If you define a variable at the top level of the `gitlab-ci.yml` file, it is global, If you define a variable at the top level of the `.gitlab-ci.yml` file, it is global,
meaning it applies to all jobs. If you define a variable in a job, it's available meaning it applies to all jobs. If you define a variable in a job, it's available
to that job only. to that job only.
......
...@@ -98,6 +98,7 @@ EE: true ...@@ -98,6 +98,7 @@ EE: true
database records created during Cycle Analytics model spec." database records created during Cycle Analytics model spec."
- _Any_ contribution from a community member, no matter how small, **may** have - _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one. a changelog entry regardless of these guidelines if the contributor wants one.
- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry.
- [Removing](feature_flags/#changelog) a feature flag, when the new code is retained. - [Removing](feature_flags/#changelog) a feature flag, when the new code is retained.
## Writing good changelog entries ## Writing good changelog entries
......
...@@ -74,7 +74,7 @@ If your application utilizes Docker containers you have another option for deplo ...@@ -74,7 +74,7 @@ If your application utilizes Docker containers you have another option for deplo
After your Docker build job completes and your image is added to your container registry, you can use the image as a After your Docker build job completes and your image is added to your container registry, you can use the image as a
[service](../../../ci/services/index.md). [service](../../../ci/services/index.md).
By using service definitions in your `gitlab-ci.yml`, you can scan services with the DAST analyzer. By using service definitions in your `.gitlab-ci.yml`, you can scan services with the DAST analyzer.
```yaml ```yaml
stages: stages:
...@@ -1307,9 +1307,9 @@ dast: ...@@ -1307,9 +1307,9 @@ dast:
By default, DAST downloads all artifacts defined by previous jobs in the pipeline. If By default, DAST downloads all artifacts defined by previous jobs in the pipeline. If
your DAST job does not rely on `environment_url.txt` to define the URL under test or any other files created your DAST job does not rely on `environment_url.txt` to define the URL under test or any other files created
in previous jobs, we recommend you don't download artifacts. To avoid downloading in previous jobs, we recommend you don't download artifacts. To avoid downloading
artifacts, add the following to your `gitlab-ci.yml` file: artifacts, add the following to your `.gitlab-ci.yml` file:
```json ```yaml
dast: dast:
dependencies: [] dependencies: []
``` ```
...@@ -111,7 +111,7 @@ example of such a transfer: ...@@ -111,7 +111,7 @@ example of such a transfer:
GitLab provides a [vendored template](../../../ci/yaml/index.md#includetemplate) GitLab provides a [vendored template](../../../ci/yaml/index.md#includetemplate)
to ease this process. to ease this process.
This template should be used in a new, empty project, with a `gitlab-ci.yml` file containing: This template should be used in a new, empty project, with a `.gitlab-ci.yml` file containing:
```yaml ```yaml
include: include:
......
...@@ -316,7 +316,7 @@ The optional `runtime` parameter can refer to one of the following runtime alias ...@@ -316,7 +316,7 @@ The optional `runtime` parameter can refer to one of the following runtime alias
| `openfaas/classic/python3` | OpenFaaS | | `openfaas/classic/python3` | OpenFaaS |
| `openfaas/classic/ruby` | OpenFaaS | | `openfaas/classic/ruby` | OpenFaaS |
After the `gitlab-ci.yml` template has been added and the `serverless.yml` file After the `.gitlab-ci.yml` template has been added and the `serverless.yml` file
has been created, pushing a commit to your project results in a CI pipeline has been created, pushing a commit to your project results in a CI pipeline
being executed which deploys each function as a Knative service. After the being executed which deploys each function as a Knative service. After the
deploy stage has finished, additional details for the function display deploy stage has finished, additional details for the function display
......
...@@ -129,7 +129,7 @@ The `source` is ignored if the path does not follow this pattern. The parser ass ...@@ -129,7 +129,7 @@ The `source` is ignored if the path does not follow this pattern. The parser ass
### JavaScript example ### JavaScript example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/) The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
JavaScript testing and [nyc](https://github.com/istanbuljs/nyc) coverage-tooling to JavaScript testing and [nyc](https://github.com/istanbuljs/nyc) coverage-tooling to
generate the coverage artifact: generate the coverage artifact:
...@@ -147,7 +147,7 @@ test: ...@@ -147,7 +147,7 @@ test:
#### Maven example #### Maven example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/) The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
generate the coverage artifact. generate the coverage artifact.
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image. You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
...@@ -185,7 +185,7 @@ coverage-jdk11: ...@@ -185,7 +185,7 @@ coverage-jdk11:
#### Gradle example #### Gradle example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/) The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
generate the coverage artifact. generate the coverage artifact.
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image. You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
...@@ -223,7 +223,7 @@ coverage-jdk11: ...@@ -223,7 +223,7 @@ coverage-jdk11:
### Python example ### Python example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths. The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
The information isn't displayed without the conversion. The information isn't displayed without the conversion.
This example assumes that the code for your package is in `src/` and your tests are in `tests.py`: This example assumes that the code for your package is in `src/` and your tests are in `tests.py`:
...@@ -243,7 +243,7 @@ run tests: ...@@ -243,7 +243,7 @@ run tests:
### C/C++ example ### C/C++ example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
`gcc` or `g++` as the compiler uses [`gcovr`](https://gcovr.com/en/stable/) to generate the coverage `gcc` or `g++` as the compiler uses [`gcovr`](https://gcovr.com/en/stable/) to generate the coverage
output file in Cobertura XML format. output file in Cobertura XML format.
......
...@@ -46,7 +46,7 @@ To create a GitLab Pages website: ...@@ -46,7 +46,7 @@ To create a GitLab Pages website:
| Document | Description | | Document | Description |
| -------- | ----------- | | -------- | ----------- |
| [Create a `gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. | | [Create a `.gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
| [Use a `.gitlab-ci.yml` template](getting_started/pages_ci_cd_template.md) | Add a Pages site to an existing project. Use a pre-populated CI template file. | | [Use a `.gitlab-ci.yml` template](getting_started/pages_ci_cd_template.md) | Add a Pages site to an existing project. Use a pre-populated CI template file. |
| [Fork a sample project](getting_started/pages_forked_sample_project.md) | Create a new project with Pages already configured by forking a sample project. | | [Fork a sample project](getting_started/pages_forked_sample_project.md) | Create a new project with Pages already configured by forking a sample project. |
| [Use a project template](getting_started/pages_new_project_template.md) | Create a new project with Pages already configured by using a template. | | [Use a project template](getting_started/pages_new_project_template.md) | Create a new project with Pages already configured by using a template. |
......
...@@ -200,7 +200,7 @@ If the job that's executing is within a freeze period, GitLab CI/CD creates an e ...@@ -200,7 +200,7 @@ If the job that's executing is within a freeze period, GitLab CI/CD creates an e
variable named `$CI_DEPLOY_FREEZE`. variable named `$CI_DEPLOY_FREEZE`.
To prevent the deployment job from executing, create a `rules` entry in your To prevent the deployment job from executing, create a `rules` entry in your
`gitlab-ci.yml`, for example: `.gitlab-ci.yml`, for example:
```yaml ```yaml
deploy_to_production: deploy_to_production:
......
...@@ -3,7 +3,7 @@ import setHighlightClass from '~/search/highlight_blob_search_result'; ...@@ -3,7 +3,7 @@ import setHighlightClass from '~/search/highlight_blob_search_result';
export default (searchTerm) => { export default (searchTerm) => {
const highlightLineClass = 'hll'; const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body'); const contentBody = document.getElementById('content-body');
const blobs = contentBody.querySelectorAll('.blob-result'); const blobs = contentBody.querySelectorAll('.js-blob-result');
// Supports Basic (backed by Gitaly) Search highlighting // Supports Basic (backed by Gitaly) Search highlighting
setHighlightClass(searchTerm); setHighlightClass(searchTerm);
......
...@@ -10,7 +10,7 @@ module Dast ...@@ -10,7 +10,7 @@ module Dast
has_many :secret_variables, through: :dast_site_profile, class_name: 'Dast::SiteProfileSecretVariable' has_many :secret_variables, through: :dast_site_profile, class_name: 'Dast::SiteProfileSecretVariable'
has_many :dast_profile_schedules, class_name: 'Dast::ProfileSchedule', foreign_key: :dast_profile_id, inverse_of: :dast_profile has_one :dast_profile_schedule, class_name: 'Dast::ProfileSchedule', foreign_key: :dast_profile_id, inverse_of: :dast_profile
validates :description, length: { maximum: 255 } validates :description, length: { maximum: 255 }
validates :name, length: { maximum: 255 }, uniqueness: { scope: :project_id }, presence: true validates :name, length: { maximum: 255 }, uniqueness: { scope: :project_id }, presence: true
......
...@@ -3,27 +3,72 @@ ...@@ -3,27 +3,72 @@
class Dast::ProfileSchedule < ApplicationRecord class Dast::ProfileSchedule < ApplicationRecord
include CronSchedulable include CronSchedulable
CRON_DEFAULT = '* * * * *'
self.table_name = 'dast_profile_schedules' self.table_name = 'dast_profile_schedules'
belongs_to :project belongs_to :project
belongs_to :dast_profile, class_name: 'Dast::Profile', optional: false, inverse_of: :dast_profile_schedules belongs_to :dast_profile, class_name: 'Dast::Profile', optional: false, inverse_of: :dast_profile_schedule
belongs_to :owner, class_name: 'User', optional: true, foreign_key: :user_id belongs_to :owner, class_name: 'User', optional: true, foreign_key: :user_id
validates :cron, presence: true validates :timezone, presence: true, inclusion: { in: :timezones }
validates :next_run_at, presence: true validates :starts_at, presence: true
validates :cadence, json_schema: { filename: 'dast_profile_schedule_cadence', draft: 7 }
validates :dast_profile_id, uniqueness: true
serialize :cadence, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
scope :with_project, -> { includes(:project) } scope :with_project, -> { includes(:project) }
scope :with_profile, -> { includes(dast_profile: [:dast_site_profile, :dast_scanner_profile]) } scope :with_profile, -> { includes(dast_profile: [:dast_site_profile, :dast_scanner_profile]) }
scope :with_owner, -> { includes(:owner) } scope :with_owner, -> { includes(:owner) }
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
before_save :set_cron, :set_next_run_at
def repeat?
cadence.present?
end
def schedule_next_run!
return deactivate! unless repeat?
super
end
def audit_details
owner&.name
end
private private
def deactivate!
update!(active: false)
end
def cron_timezone def cron_timezone
next_run_at.zone Time.zone.name
end
def set_cron
self.cron =
if repeat?
Gitlab::Ci::CronParser.parse_natural_with_timestamp(starts_at, cadence)
else
CRON_DEFAULT
end
end
def set_next_run_at
return super unless will_save_change_to_starts_at?
self.next_run_at = cron_worker_next_run_from(starts_at)
end end
def worker_cron_expression def worker_cron_expression
Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['cron'] Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['cron']
end end
def timezones
@timezones ||= ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier }
end
end end
...@@ -5,7 +5,8 @@ FactoryBot.define do ...@@ -5,7 +5,8 @@ FactoryBot.define do
project project
dast_profile dast_profile
owner { association(:user) } owner { association(:user) }
cron { '*/10 * * * *' } timezone { FFaker::Address.time_zone }
next_run_at { Time.now } starts_at { Time.now }
cadence { { unit: %w(day month year week).sample, duration: 1 } }
end end
end end
...@@ -11,7 +11,6 @@ describe('Environment', () => { ...@@ -11,7 +11,6 @@ describe('Environment', () => {
const mockData = { const mockData = {
canCreateEnvironment: true, canCreateEnvironment: true,
canReadEnvironment: true,
endpoint: 'environments.json', endpoint: 'environments.json',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
helpPagePath: 'help', helpPagePath: 'help',
......
...@@ -36,7 +36,6 @@ describe('Environment table', () => { ...@@ -36,7 +36,6 @@ describe('Environment table', () => {
{ {
propsData: { propsData: {
environments: [mockItem], environments: [mockItem],
canReadEnvironment: true,
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
......
...@@ -11,7 +11,7 @@ describe('ee/search/highlight_blob_search_result', () => { ...@@ -11,7 +11,7 @@ describe('ee/search/highlight_blob_search_result', () => {
setHighlightClass(searchKeyword); setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4); expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
}); });
// Advanced search support // Advanced search support
...@@ -20,6 +20,6 @@ describe('ee/search/highlight_blob_search_result', () => { ...@@ -20,6 +20,6 @@ describe('ee/search/highlight_blob_search_result', () => {
setHighlightClass(searchKeyword); setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(3); expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(3);
}); });
}); });
...@@ -7,14 +7,52 @@ RSpec.describe Dast::ProfileSchedule, type: :model do ...@@ -7,14 +7,52 @@ RSpec.describe Dast::ProfileSchedule, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:dast_profile).class_name('Dast::Profile').required.inverse_of(:dast_profile_schedules) } it { is_expected.to belong_to(:dast_profile).class_name('Dast::Profile').required.inverse_of(:dast_profile_schedule) }
it { is_expected.to belong_to(:owner).class_name('User').with_foreign_key(:user_id) } it { is_expected.to belong_to(:owner).class_name('User').with_foreign_key(:user_id) }
end end
describe 'validations' do describe 'validations' do
let(:timezones) { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier } }
it { is_expected.to be_valid } it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:cron) } it { is_expected.to validate_presence_of(:timezone) }
it { is_expected.to validate_presence_of(:next_run_at) } it { is_expected.to validate_inclusion_of(:timezone).in_array(timezones) }
it { is_expected.to validate_presence_of(:starts_at) }
it { is_expected.to validate_uniqueness_of(:dast_profile_id) }
describe 'cadence' do
context 'when valid values' do
[
{ unit: 'day', duration: 1 },
{ unit: 'week', duration: 1 },
{ unit: 'month', duration: 1 },
{ unit: 'month', duration: 3 },
{ unit: 'month', duration: 6 },
{ unit: 'year', duration: 1 },
{}
].each do |cadence|
it "allows #{cadence[:unit]} values" do
schedule = build(:dast_profile_schedule, cadence: cadence)
expect(schedule).to be_valid
expect(schedule.cadence).to eq(cadence.stringify_keys)
end
end
end
context 'when invalid values' do
[
{ unit: 'day', duration: 3 },
{ unit: 'month_foo', duration: 100 }
].each do |cadence|
it "disallow #{cadence[:unit]} values" do
expect { build(:dast_profile_schedule, cadence: cadence).validate! }.to raise_error(ActiveRecord::RecordInvalid) do |err|
expect(err.record.errors.full_messages).to include('Cadence must be a valid json schema')
end
end
end
end
end
end end
describe 'scopes' do describe 'scopes' do
...@@ -31,13 +69,13 @@ RSpec.describe Dast::ProfileSchedule, type: :model do ...@@ -31,13 +69,13 @@ RSpec.describe Dast::ProfileSchedule, type: :model do
end end
end end
describe 'runnable_schedules' do describe '.runnable_schedules' do
subject { described_class.runnable_schedules } subject { described_class.runnable_schedules }
context 'when there are runnable schedules' do context 'when there are runnable schedules' do
let!(:profile_schedule) do let!(:profile_schedule) do
travel_to(1.day.ago) do travel_to(2.days.ago) do
create(:dast_profile_schedule) create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 })
end end
end end
...@@ -80,15 +118,73 @@ RSpec.describe Dast::ProfileSchedule, type: :model do ...@@ -80,15 +118,73 @@ RSpec.describe Dast::ProfileSchedule, type: :model do
end end
end end
describe 'before_save' do
describe '#set_cron' do
context 'when repeat? is true' do
it 'sets the cron value' do
freeze_time do
cron_statement = Gitlab::Ci::CronParser.parse_natural_with_timestamp(subject.starts_at, subject.cadence)
expect(subject.cron).to eq cron_statement
end
end
end
context 'when repeat? is false' do
subject { create(:dast_profile_schedule, cadence: {}) }
it 'sets the cron value to default when non repeating' do
expect(subject.cron).to eq Dast::ProfileSchedule::CRON_DEFAULT
end
end
end
end
describe '#set_next_run_at' do describe '#set_next_run_at' do
it_behaves_like 'handles set_next_run_at' do let(:schedule) { create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 }, starts_at: Time.zone.now) }
let(:schedule) { create(:dast_profile_schedule, cron: '*/1 * * * *') } let(:schedule_1) { create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 }) }
let(:schedule_1) { create(:dast_profile_schedule) } let(:schedule_2) { create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 }) }
let(:schedule_2) { create(:dast_profile_schedule) }
let(:new_cron) { '0 0 1 1 *' } let(:cron_worker_next_run_at) { schedule.send(:cron_worker_next_run_from, Time.zone.now) }
let(:ideal_next_run_at) { schedule.send(:ideal_next_run_from, Time.zone.now) } context 'when schedule runs every minute' do
let(:cron_worker_next_run_at) { schedule.send(:cron_worker_next_run_from, Time.zone.now) } it "updates next_run_at to the worker's execution time" do
travel_to(1.day.ago) do
expect(schedule.next_run_at.to_i).to eq(cron_worker_next_run_at.to_i)
end
end
end
context 'when there are two different schedules in the same time zones' do
it 'sets the sames next_run_at' do
expect(schedule_1.next_run_at.to_i).to eq(schedule_2.next_run_at.to_i)
end
end
context 'when starts_at is updated for existing schedules' do
it 'updates next_run_at automatically' do
expect { schedule.update!(starts_at: Time.zone.now + 2.days) }.to change { schedule.next_run_at }
end
end
end
describe '#schedule_next_run!' do
context 'when repeat? is true' do
it 'sets active to true' do
subject.schedule_next_run!
expect(subject.active).to be true
end
end
context 'when repeat? is false' do
it 'sets active to false' do
subject.update_column(:cadence, {})
subject.schedule_next_run!
expect(subject.active).to be false
end
end end
end end
end end
...@@ -12,7 +12,7 @@ RSpec.describe Dast::Profile, type: :model do ...@@ -12,7 +12,7 @@ RSpec.describe Dast::Profile, type: :model do
it { is_expected.to belong_to(:dast_site_profile) } it { is_expected.to belong_to(:dast_site_profile) }
it { is_expected.to belong_to(:dast_scanner_profile) } it { is_expected.to belong_to(:dast_scanner_profile) }
it { is_expected.to have_many(:secret_variables).through(:dast_site_profile).class_name('Dast::SiteProfileSecretVariable') } it { is_expected.to have_many(:secret_variables).through(:dast_site_profile).class_name('Dast::SiteProfileSecretVariable') }
it { is_expected.to have_many(:dast_profile_schedules).class_name('Dast::ProfileSchedule').with_foreign_key(:dast_profile_id).inverse_of(:dast_profile) } it { is_expected.to have_one(:dast_profile_schedule).class_name('Dast::ProfileSchedule').with_foreign_key(:dast_profile_id).inverse_of(:dast_profile) }
end end
describe 'validations' do describe 'validations' do
......
...@@ -101,5 +101,19 @@ RSpec.describe AppSec::Dast::ProfileScheduleWorker do ...@@ -101,5 +101,19 @@ RSpec.describe AppSec::Dast::ProfileScheduleWorker do
subject subject
end end
end end
context 'when single run schedule exists' do
before do
schedule.update_columns(next_run_at: 1.minute.ago, cadence: {})
end
it 'executes the rule schedule service and deactivate the schedule', :aggregate_failures do
expect(schedule.repeat?).to be(false)
subject
expect(schedule.reload.active).to be(false)
end
end
end end
end end
...@@ -6,8 +6,40 @@ module Gitlab ...@@ -6,8 +6,40 @@ module Gitlab
VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC' VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'
VALID_SYNTAX_SAMPLE_CRON = '* * * * *' VALID_SYNTAX_SAMPLE_CRON = '* * * * *'
def self.parse_natural(expression, cron_timezone = 'UTC') class << self
new(Fugit::Nat.parse(expression)&.original, cron_timezone) def parse_natural(expression, cron_timezone = 'UTC')
new(Fugit::Nat.parse(expression)&.original, cron_timezone)
end
# This method generates compatible expressions that can be
# parsed by Fugit::Nat.parse to generate a cron line.
# It takes start date of the cron and cadence in the following format:
# cadence = {
# unit: 'day/week/month/year'
# duration: 1
# }
def parse_natural_with_timestamp(starts_at, cadence)
case cadence[:unit]
when 'day' # Currently supports only 'every 1 day'.
"#{starts_at.min} #{starts_at.hour} * * *"
when 'week' # Currently supports only 'every 1 week'.
"#{starts_at.min} #{starts_at.hour} * * #{starts_at.wday}"
when 'month'
unless [1, 3, 6, 12].include?(cadence[:duration])
raise NotImplementedError, "The cadence #{cadence} is not supported"
end
"#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{fall_in_months(cadence[:duration], starts_at)} *"
when 'year' # Currently supports only 'every 1 year'.
"#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{starts_at.month} *"
else
raise NotImplementedError, "The cadence unit #{cadence[:unit]} is not implemented"
end
end
def fall_in_months(offset, start_date)
(1..(12 / offset)).map { |i| start_date.next_month(offset * i).month }.join(',')
end
end end
def initialize(cron, cron_timezone = 'UTC') def initialize(cron, cron_timezone = 'UTC')
......
...@@ -29006,6 +29006,9 @@ msgstr "" ...@@ -29006,6 +29006,9 @@ msgstr ""
msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor." msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor."
msgstr "" msgstr ""
msgid "Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?"
msgstr ""
msgid "Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner." msgid "Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner."
msgstr "" msgstr ""
......
...@@ -311,23 +311,42 @@ RSpec.describe Projects::PipelinesController do ...@@ -311,23 +311,42 @@ RSpec.describe Projects::PipelinesController do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
def create_build_with_artifacts(stage, stage_idx, name) def create_build_with_artifacts(stage, stage_idx, name, status)
create(:ci_build, :artifacts, :tags, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name) create(:ci_build, :artifacts, :tags, status, user: user, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
end
def create_bridge(stage, stage_idx, name, status)
create(:ci_bridge, status, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
end end
before do before do
create_build_with_artifacts('build', 0, 'job1') create_build_with_artifacts('build', 0, 'job1', :failed)
create_build_with_artifacts('build', 0, 'job2') create_build_with_artifacts('build', 0, 'job2', :running)
create_build_with_artifacts('build', 0, 'job3', :pending)
create_bridge('deploy', 1, 'deploy-a', :failed)
create_bridge('deploy', 1, 'deploy-b', :created)
end end
it 'avoids N+1 database queries', :request_store do it 'avoids N+1 database queries', :request_store, :use_sql_query_cache do
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_html }.count # warm up
get_pipeline_html
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
create_build_with_artifacts('build', 0, 'job3') control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
end
create_build_with_artifacts('build', 0, 'job4', :failed)
create_build_with_artifacts('build', 0, 'job5', :running)
create_build_with_artifacts('build', 0, 'job6', :pending)
create_bridge('deploy', 1, 'deploy-c', :failed)
create_bridge('deploy', 1, 'deploy-d', :created)
expect { get_pipeline_html }.not_to exceed_query_limit(control_count) expect do
expect(response).to have_gitlab_http_status(:ok) get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
end.not_to exceed_all_query_limit(control)
end end
end end
......
...@@ -31,7 +31,6 @@ describe('Environment item', () => { ...@@ -31,7 +31,6 @@ describe('Environment item', () => {
factory({ factory({
propsData: { propsData: {
model: environment, model: environment,
canReadEnvironment: true,
tableData, tableData,
}, },
}); });
...@@ -135,7 +134,6 @@ describe('Environment item', () => { ...@@ -135,7 +134,6 @@ describe('Environment item', () => {
factory({ factory({
propsData: { propsData: {
model: environmentWithoutDeployable, model: environmentWithoutDeployable,
canReadEnvironment: true,
tableData, tableData,
}, },
}); });
...@@ -161,7 +159,6 @@ describe('Environment item', () => { ...@@ -161,7 +159,6 @@ describe('Environment item', () => {
factory({ factory({
propsData: { propsData: {
model: environmentWithoutUpcomingDeployment, model: environmentWithoutUpcomingDeployment,
canReadEnvironment: true,
tableData, tableData,
}, },
}); });
...@@ -177,7 +174,6 @@ describe('Environment item', () => { ...@@ -177,7 +174,6 @@ describe('Environment item', () => {
factory({ factory({
propsData: { propsData: {
model: environment, model: environment,
canReadEnvironment: true,
tableData, tableData,
shouldShowAutoStopDate: true, shouldShowAutoStopDate: true,
}, },
...@@ -205,7 +201,6 @@ describe('Environment item', () => { ...@@ -205,7 +201,6 @@ describe('Environment item', () => {
...environment, ...environment,
auto_stop_at: futureDate, auto_stop_at: futureDate,
}, },
canReadEnvironment: true,
tableData, tableData,
shouldShowAutoStopDate: true, shouldShowAutoStopDate: true,
}, },
...@@ -241,7 +236,6 @@ describe('Environment item', () => { ...@@ -241,7 +236,6 @@ describe('Environment item', () => {
...environment, ...environment,
auto_stop_at: pastDate, auto_stop_at: pastDate,
}, },
canReadEnvironment: true,
tableData, tableData,
shouldShowAutoStopDate: true, shouldShowAutoStopDate: true,
}, },
...@@ -360,7 +354,6 @@ describe('Environment item', () => { ...@@ -360,7 +354,6 @@ describe('Environment item', () => {
factory({ factory({
propsData: { propsData: {
model: folder, model: folder,
canReadEnvironment: true,
tableData, tableData,
}, },
}); });
......
...@@ -28,7 +28,6 @@ describe('Environment table', () => { ...@@ -28,7 +28,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: [folder], environments: [folder],
canReadEnvironment: true,
...eeOnlyProps, ...eeOnlyProps,
}, },
}); });
...@@ -50,7 +49,6 @@ describe('Environment table', () => { ...@@ -50,7 +49,6 @@ describe('Environment table', () => {
await factory({ await factory({
propsData: { propsData: {
environments: [mockItem], environments: [mockItem],
canReadEnvironment: true,
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
...@@ -78,7 +76,6 @@ describe('Environment table', () => { ...@@ -78,7 +76,6 @@ describe('Environment table', () => {
propsData: { propsData: {
environments: [mockItem], environments: [mockItem],
canCreateDeployment: false, canCreateDeployment: false,
canReadEnvironment: true,
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
...@@ -114,7 +111,6 @@ describe('Environment table', () => { ...@@ -114,7 +111,6 @@ describe('Environment table', () => {
propsData: { propsData: {
environments: [mockItem], environments: [mockItem],
canCreateDeployment: false, canCreateDeployment: false,
canReadEnvironment: true,
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
...@@ -151,7 +147,6 @@ describe('Environment table', () => { ...@@ -151,7 +147,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: [mockItem], environments: [mockItem],
canReadEnvironment: true,
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
...@@ -179,7 +174,6 @@ describe('Environment table', () => { ...@@ -179,7 +174,6 @@ describe('Environment table', () => {
propsData: { propsData: {
environments: [mockItem], environments: [mockItem],
canCreateDeployment: false, canCreateDeployment: false,
canReadEnvironment: true,
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments', helpCanaryDeploymentsPath: 'help/canary-deployments',
...@@ -230,7 +224,6 @@ describe('Environment table', () => { ...@@ -230,7 +224,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: mockItems, environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps, ...eeOnlyProps,
}, },
}); });
...@@ -296,7 +289,6 @@ describe('Environment table', () => { ...@@ -296,7 +289,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: mockItems, environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps, ...eeOnlyProps,
}, },
}); });
...@@ -335,7 +327,6 @@ describe('Environment table', () => { ...@@ -335,7 +327,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: mockItems, environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps, ...eeOnlyProps,
}, },
}); });
...@@ -364,7 +355,6 @@ describe('Environment table', () => { ...@@ -364,7 +355,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: mockItems, environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps, ...eeOnlyProps,
}, },
}); });
...@@ -415,7 +405,6 @@ describe('Environment table', () => { ...@@ -415,7 +405,6 @@ describe('Environment table', () => {
factory({ factory({
propsData: { propsData: {
environments: mockItems, environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps, ...eeOnlyProps,
}, },
}); });
......
...@@ -20,7 +20,6 @@ describe('Environment', () => { ...@@ -20,7 +20,6 @@ describe('Environment', () => {
const mockData = { const mockData = {
endpoint: 'environments.json', endpoint: 'environments.json',
canCreateEnvironment: true, canCreateEnvironment: true,
canReadEnvironment: true,
newEnvironmentPath: 'environments/new', newEnvironmentPath: 'environments/new',
helpPagePath: 'help', helpPagePath: 'help',
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
......
...@@ -44,7 +44,6 @@ describe('Environments detail header component', () => { ...@@ -44,7 +44,6 @@ describe('Environments detail header component', () => {
TimeAgo, TimeAgo,
}, },
propsData: { propsData: {
canReadEnvironment: false,
canAdminEnvironment: false, canAdminEnvironment: false,
canUpdateEnvironment: false, canUpdateEnvironment: false,
canStopEnvironment: false, canStopEnvironment: false,
...@@ -60,7 +59,7 @@ describe('Environments detail header component', () => { ...@@ -60,7 +59,7 @@ describe('Environments detail header component', () => {
describe('default state with minimal access', () => { describe('default state with minimal access', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ props: { environment: createEnvironment() } }); createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
}); });
it('displays the environment name', () => { it('displays the environment name', () => {
...@@ -164,7 +163,6 @@ describe('Environments detail header component', () => { ...@@ -164,7 +163,6 @@ describe('Environments detail header component', () => {
createWrapper({ createWrapper({
props: { props: {
environment: createEnvironment({ hasTerminals: true, externalUrl }), environment: createEnvironment({ hasTerminals: true, externalUrl }),
canReadEnvironment: true,
}, },
}); });
}); });
...@@ -178,8 +176,7 @@ describe('Environments detail header component', () => { ...@@ -178,8 +176,7 @@ describe('Environments detail header component', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ createWrapper({
props: { props: {
environment: createEnvironment(), environment: createEnvironment({ metricsUrl: 'my metrics url' }),
canReadEnvironment: true,
metricsPath, metricsPath,
}, },
}); });
...@@ -195,7 +192,6 @@ describe('Environments detail header component', () => { ...@@ -195,7 +192,6 @@ describe('Environments detail header component', () => {
createWrapper({ createWrapper({
props: { props: {
environment: createEnvironment(), environment: createEnvironment(),
canReadEnvironment: true,
canAdminEnvironment: true, canAdminEnvironment: true,
canStopEnvironment: true, canStopEnvironment: true,
canUpdateEnvironment: true, canUpdateEnvironment: true,
......
...@@ -11,7 +11,6 @@ describe('Environments Folder View', () => { ...@@ -11,7 +11,6 @@ describe('Environments Folder View', () => {
const mockData = { const mockData = {
endpoint: 'environments.json', endpoint: 'environments.json',
folderName: 'review', folderName: 'review',
canReadEnvironment: true,
cssContainerClass: 'container', cssContainerClass: 'container',
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
......
...@@ -14,7 +14,6 @@ describe('Environments Folder View', () => { ...@@ -14,7 +14,6 @@ describe('Environments Folder View', () => {
const mockData = { const mockData = {
endpoint: 'environments.json', endpoint: 'environments.json',
folderName: 'review', folderName: 'review',
canReadEnvironment: true,
cssContainerClass: 'container', cssContainerClass: 'container',
userCalloutsPath: '/callouts', userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg', lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
......
import { shallowMount } from '@vue/test-utils';
import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
describe('Image Viewer', () => {
let wrapper;
const propsData = {
url: 'some/image.png',
alt: 'image.png',
};
const createComponent = () => {
wrapper = shallowMount(ImageViewer, { propsData });
};
const findImage = () => wrapper.find('[data-testid="image"]');
it('renders a Source Editor component', () => {
createComponent();
expect(findImage().exists()).toBe(true);
expect(findImage().attributes('src')).toBe(propsData.url);
expect(findImage().attributes('alt')).toBe(propsData.alt);
});
});
...@@ -9,6 +9,6 @@ describe('search/highlight_blob_search_result', () => { ...@@ -9,6 +9,6 @@ describe('search/highlight_blob_search_result', () => {
it('highlights lines with search term occurrence', () => { it('highlights lines with search term occurrence', () => {
setHighlightClass(searchKeyword); setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4); expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
}); });
}); });
...@@ -43,7 +43,6 @@ RSpec.describe EnvironmentHelper do ...@@ -43,7 +43,6 @@ RSpec.describe EnvironmentHelper do
external_url: environment.external_url, external_url: environment.external_url,
can_update_environment: true, can_update_environment: true,
can_destroy_environment: true, can_destroy_environment: true,
can_read_environment: true,
can_stop_environment: true, can_stop_environment: true,
can_admin_environment: true, can_admin_environment: true,
environment_metrics_path: environment_metrics_path(environment), environment_metrics_path: environment_metrics_path(environment),
......
...@@ -297,4 +297,65 @@ RSpec.describe Gitlab::Ci::CronParser do ...@@ -297,4 +297,65 @@ RSpec.describe Gitlab::Ci::CronParser do
it { is_expected.to eq(true) } it { is_expected.to eq(true) }
end end
end end
describe '.parse_natural', :aggregate_failures do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'day', duration: 1 }) }
let(:time) { Time.parse('Mon, 30 Aug 2021 06:29:44.067132000 UTC +00:00') }
let(:hours) { Fugit::Cron.parse(cron_line).hours }
let(:minutes) { Fugit::Cron.parse(cron_line).minutes }
let(:weekdays) { Fugit::Cron.parse(cron_line).weekdays.first }
let(:months) { Fugit::Cron.parse(cron_line).months }
context 'when repeat cycle is day' do
it 'generates daily cron expression', :aggregate_failures do
expect(hours).to include time.hour
expect(minutes).to include time.min
end
end
context 'when repeat cycle is week' do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'week', duration: 1 }) }
it 'generates weekly cron expression', :aggregate_failures do
expect(hours).to include time.hour
expect(minutes).to include time.min
expect(weekdays).to include time.wday
end
end
context 'when repeat cycle is month' do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 3 }) }
it 'generates monthly cron expression', :aggregate_failures do
expect(minutes).to include time.min
expect(months).to include time.month
end
context 'when an unsupported duration is specified' do
subject { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 7 }) }
it 'raises an exception' do
expect { subject }.to raise_error(NotImplementedError, 'The cadence {:unit=>"month", :duration=>7} is not supported')
end
end
end
context 'when repeat cycle is year' do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'year', duration: 1 }) }
it 'generates yearly cron expression', :aggregate_failures do
expect(hours).to include time.hour
expect(minutes).to include time.min
expect(months).to include time.month
end
end
context 'when the repeat cycle is not implemented' do
subject { described_class.parse_natural_with_timestamp(time, { unit: 'quarterly', duration: 1 }) }
it 'raises an exception' do
expect { subject }.to raise_error(NotImplementedError, 'The cadence unit quarterly is not implemented')
end
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment