Commit 0ff11dd2 authored by Sean McGivern's avatar Sean McGivern

Merge branch '328749-remove-dast-scheduler-feature-flag' into 'master'

Remove the dast_on_demand_scans_scheduler feature flag

See merge request gitlab-org/gitlab!72953
parents c9fe8599 958bab52
...@@ -1554,7 +1554,7 @@ Input type: `DastProfileCreateInput` ...@@ -1554,7 +1554,7 @@ Input type: `DastProfileCreateInput`
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="mutationdastprofilecreatebranchname"></a>`branchName` | [`String`](#string) | Associated branch. | | <a id="mutationdastprofilecreatebranchname"></a>`branchName` | [`String`](#string) | Associated branch. |
| <a id="mutationdastprofilecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationdastprofilecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastprofilecreatedastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST Profile Schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled. | | <a id="mutationdastprofilecreatedastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST Profile Schedule. |
| <a id="mutationdastprofilecreatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID!`](#dastscannerprofileid) | ID of the scanner profile to be associated. | | <a id="mutationdastprofilecreatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID!`](#dastscannerprofileid) | ID of the scanner profile to be associated. |
| <a id="mutationdastprofilecreatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be associated. | | <a id="mutationdastprofilecreatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be associated. |
| <a id="mutationdastprofilecreatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. | | <a id="mutationdastprofilecreatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. |
...@@ -1619,7 +1619,7 @@ Input type: `DastProfileUpdateInput` ...@@ -1619,7 +1619,7 @@ Input type: `DastProfileUpdateInput`
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="mutationdastprofileupdatebranchname"></a>`branchName` | [`String`](#string) | Associated branch. | | <a id="mutationdastprofileupdatebranchname"></a>`branchName` | [`String`](#string) | Associated branch. |
| <a id="mutationdastprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationdastprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastprofileupdatedastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST profile schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled. | | <a id="mutationdastprofileupdatedastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST profile schedule. |
| <a id="mutationdastprofileupdatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID`](#dastscannerprofileid) | ID of the scanner profile to be associated. | | <a id="mutationdastprofileupdatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID`](#dastscannerprofileid) | ID of the scanner profile to be associated. |
| <a id="mutationdastprofileupdatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile to be associated. | | <a id="mutationdastprofileupdatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile to be associated. |
| <a id="mutationdastprofileupdatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. | | <a id="mutationdastprofileupdatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. |
...@@ -8951,7 +8951,7 @@ Represents a DAST Profile. ...@@ -8951,7 +8951,7 @@ Represents a DAST Profile.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="dastprofilebranch"></a>`branch` | [`DastProfileBranch`](#dastprofilebranch) | Associated branch. | | <a id="dastprofilebranch"></a>`branch` | [`DastProfileBranch`](#dastprofilebranch) | Associated branch. |
| <a id="dastprofiledastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileSchedule`](#dastprofileschedule) | Associated profile schedule. Will always return `null` if `dast_on_demand_scans_scheduler` feature flag is disabled. | | <a id="dastprofiledastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileSchedule`](#dastprofileschedule) | Associated profile schedule. |
| <a id="dastprofiledastscannerprofile"></a>`dastScannerProfile` | [`DastScannerProfile`](#dastscannerprofile) | Associated scanner profile. | | <a id="dastprofiledastscannerprofile"></a>`dastScannerProfile` | [`DastScannerProfile`](#dastscannerprofile) | Associated scanner profile. |
| <a id="dastprofiledastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | Associated site profile. | | <a id="dastprofiledastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | Associated site profile. |
| <a id="dastprofiledescription"></a>`description` | [`String`](#string) | Description of the scan. | | <a id="dastprofiledescription"></a>`description` | [`String`](#string) | Description of the scan. |
......
...@@ -1017,12 +1017,7 @@ The on-demand DAST scan runs, and the project's dashboard shows the results. ...@@ -1017,12 +1017,7 @@ The on-demand DAST scan runs, and the project's dashboard shows the results.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.3. [Deployed behind the `dast_on_demand_scans_scheduler` flag](../../../administration/feature_flags.md), disabled by default. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.3. [Deployed behind the `dast_on_demand_scans_scheduler` flag](../../../administration/feature_flags.md), disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.4. > - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.4.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.4. > - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.4.
> - [Feature flag dast_on_demand_scans_scheduler removed](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.5.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an
administrator to [disable the feature flag](../../../administration/feature_flags.md) named
`dast_on_demand_scans_scheduler`.
On GitLab.com, this feature is available.
To schedule a scan: To schedule a scan:
......
...@@ -26,7 +26,6 @@ import RefSelector from '~/ref/components/ref_selector.vue'; ...@@ -26,7 +26,6 @@ import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES } from '~/ref/constants'; import { REF_TYPE_BRANCHES } from '~/ref/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import validation from '~/vue_shared/directives/validation'; import validation from '~/vue_shared/directives/validation';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import dastProfileCreateMutation from '../graphql/dast_profile_create.mutation.graphql'; import dastProfileCreateMutation from '../graphql/dast_profile_create.mutation.graphql';
import dastProfileUpdateMutation from '../graphql/dast_profile_update.mutation.graphql'; import dastProfileUpdateMutation from '../graphql/dast_profile_update.mutation.graphql';
import { import {
...@@ -93,7 +92,6 @@ export default { ...@@ -93,7 +92,6 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
validation: validation(), validation: validation(),
}, },
mixins: [glFeatureFlagMixin()],
apollo: { apollo: {
scannerProfiles: createProfilesApolloOptions( scannerProfiles: createProfilesApolloOptions(
'scannerProfiles', 'scannerProfiles',
...@@ -250,9 +248,7 @@ export default { ...@@ -250,9 +248,7 @@ export default {
dastScannerProfileId: this.selectedScannerProfile.id, dastScannerProfileId: this.selectedScannerProfile.id,
dastSiteProfileId: this.selectedSiteProfile.id, dastSiteProfileId: this.selectedSiteProfile.id,
branchName: this.selectedBranch, branchName: this.selectedBranch,
...(this.glFeatures.dastOnDemandScansScheduler dastProfileSchedule: this.profileSchedule,
? { dastProfileSchedule: this.profileSchedule }
: {}),
...(this.isEdit ? { id: this.dastScan.id } : { fullPath: this.projectPath }), ...(this.isEdit ? { id: this.dastScan.id } : { fullPath: this.projectPath }),
...serializeFormObject(this.form.fields), ...serializeFormObject(this.form.fields),
[this.isEdit ? 'runAfterUpdate' : 'runAfterCreate']: runAfter, [this.isEdit ? 'runAfterUpdate' : 'runAfterCreate']: runAfter,
...@@ -456,11 +452,7 @@ export default { ...@@ -456,11 +452,7 @@ export default {
:has-conflict="hasProfilesConflict" :has-conflict="hasProfilesConflict"
/> />
<scan-schedule <scan-schedule v-model="profileSchedule" class="gl-mb-5" />
v-if="glFeatures.dastOnDemandScansScheduler"
v-model="profileSchedule"
class="gl-mb-5"
/>
<profile-conflict-alert <profile-conflict-alert
v-if="hasProfilesConflict" v-if="hasProfilesConflict"
......
...@@ -8,10 +8,6 @@ module Projects ...@@ -8,10 +8,6 @@ module Projects
before_action :authorize_read_on_demand_dast_scan!, only: :index before_action :authorize_read_on_demand_dast_scan!, only: :index
before_action :authorize_create_on_demand_dast_scan!, only: [:new, :edit] before_action :authorize_create_on_demand_dast_scan!, only: [:new, :edit]
before_action do
push_frontend_feature_flag(:dast_on_demand_scans_scheduler, @project, default_enabled: :yaml)
end
feature_category :dynamic_application_security_testing feature_category :dynamic_application_security_testing
def index def index
......
...@@ -22,7 +22,7 @@ module Mutations ...@@ -22,7 +22,7 @@ module Mutations
argument :dast_profile_schedule, ::Types::Dast::ProfileScheduleInputType, argument :dast_profile_schedule, ::Types::Dast::ProfileScheduleInputType,
required: false, required: false,
description: 'Represents a DAST Profile Schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled.' description: 'Represents a DAST Profile Schedule.'
argument :name, GraphQL::Types::String, argument :name, GraphQL::Types::String,
required: true, required: true,
...@@ -54,7 +54,6 @@ module Mutations ...@@ -54,7 +54,6 @@ module Mutations
def resolve(full_path:, name:, description: '', branch_name: nil, dast_site_profile_id:, dast_scanner_profile_id:, run_after_create: false, dast_profile_schedule: nil) def resolve(full_path:, name:, description: '', branch_name: nil, dast_site_profile_id:, dast_scanner_profile_id:, run_after_create: false, dast_profile_schedule: nil)
project = authorized_find!(full_path) project = authorized_find!(full_path)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project, dast_profile_schedule)
# TODO: remove explicit coercion once compatibility layer is removed # TODO: remove explicit coercion once compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
...@@ -86,16 +85,6 @@ module Mutations ...@@ -86,16 +85,6 @@ module Mutations
private private
def allowed?(project, dast_profile_schedule)
scheduler_flag_enabled?(dast_profile_schedule, project)
end
def scheduler_flag_enabled?(dast_profile_schedule, project)
return true unless dast_profile_schedule
Feature.enabled?(:dast_on_demand_scans_scheduler, project, default_enabled: :yaml)
end
def build_response(payload) def build_response(payload)
{ {
errors: [], errors: [],
......
...@@ -33,7 +33,7 @@ module Mutations ...@@ -33,7 +33,7 @@ module Mutations
argument :dast_profile_schedule, ::Types::Dast::ProfileScheduleInputType, argument :dast_profile_schedule, ::Types::Dast::ProfileScheduleInputType,
required: false, required: false,
description: 'Represents a DAST profile schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled.' description: 'Represents a DAST profile schedule.'
argument :name, GraphQL::Types::String, argument :name, GraphQL::Types::String,
required: false, required: false,
...@@ -65,7 +65,6 @@ module Mutations ...@@ -65,7 +65,6 @@ module Mutations
def resolve(id:, name:, description:, full_path: nil, branch_name: nil, dast_scanner_profile_id: nil, run_after_update: false, **args) def resolve(id:, name:, description:, full_path: nil, branch_name: nil, dast_scanner_profile_id: nil, run_after_update: false, **args)
dast_profile = authorized_find!(id) dast_profile = authorized_find!(id)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(args[:dast_profile_schedule], dast_profile.project)
params = { params = {
dast_profile: dast_profile, dast_profile: dast_profile,
...@@ -89,16 +88,6 @@ module Mutations ...@@ -89,16 +88,6 @@ module Mutations
private private
def allowed?(dast_profile_schedule, project)
scheduler_flag_enabled?(dast_profile_schedule, project)
end
def scheduler_flag_enabled?(dast_profile_schedule, project)
return true unless dast_profile_schedule
Feature.enabled?(:dast_on_demand_scans_scheduler, project, default_enabled: :yaml)
end
def as_model_id(klass, value) def as_model_id(klass, value)
return unless value return unless value
......
...@@ -24,8 +24,7 @@ module Types ...@@ -24,8 +24,7 @@ module Types
description: 'Associated scanner profile.' description: 'Associated scanner profile.'
field :dast_profile_schedule, ::Types::Dast::ProfileScheduleType, null: true, field :dast_profile_schedule, ::Types::Dast::ProfileScheduleType, null: true,
description: 'Associated profile schedule. Will always return `null` ' \ description: 'Associated profile schedule.'
'if `dast_on_demand_scans_scheduler` feature flag is disabled.'
field :branch, Dast::ProfileBranchType, null: true, field :branch, Dast::ProfileBranchType, null: true,
description: 'Associated branch.', description: 'Associated branch.',
...@@ -39,8 +38,6 @@ module Types ...@@ -39,8 +38,6 @@ module Types
end end
def dast_profile_schedule def dast_profile_schedule
return unless Feature.enabled?(:dast_on_demand_scans_scheduler, object.project, default_enabled: :yaml)
object.dast_profile_schedule object.dast_profile_schedule
end end
end end
......
...@@ -14,8 +14,6 @@ module AppSec ...@@ -14,8 +14,6 @@ module AppSec
data_consistency :always data_consistency :always
def perform def perform
return unless Feature.enabled?(:dast_on_demand_scans_scheduler, default_enabled: :yaml)
dast_runnable_schedules.find_in_batches do |schedules| dast_runnable_schedules.find_in_batches do |schedules|
schedules.each do |schedule| schedules.each do |schedule|
if schedule.owner_valid? if schedule.owner_valid?
......
---
name: dast_on_demand_scans_scheduler
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65327
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328749
milestone: '14.1'
type: development
group: group::dynamic analysis
default_enabled: true
...@@ -162,9 +162,6 @@ describe('OnDemandScansForm', () => { ...@@ -162,9 +162,6 @@ describe('OnDemandScansForm', () => {
newScannerProfilePath, newScannerProfilePath,
newSiteProfilePath, newSiteProfilePath,
dastSiteValidationDocsPath, dastSiteValidationDocsPath,
glFeatures: {
dastOnDemandScansScheduler: true,
},
}, },
stubs: { stubs: {
GlFormInput: GlFormInputStub, GlFormInput: GlFormInputStub,
...@@ -672,16 +669,4 @@ describe('OnDemandScansForm', () => { ...@@ -672,16 +669,4 @@ describe('OnDemandScansForm', () => {
); );
}); });
}); });
it('does not render scan schedule when the feature flag is disabled', () => {
createComponent({
provide: {
glFeatures: {
dastOnDemandScansScheduler: false,
},
},
});
expect(wrapper.findComponent(ScanSchedule).exists()).toBe(false);
});
}); });
...@@ -12,7 +12,6 @@ RSpec.describe Mutations::Dast::Profiles::Create do ...@@ -12,7 +12,6 @@ RSpec.describe Mutations::Dast::Profiles::Create do
let(:description) { SecureRandom.hex } let(:description) { SecureRandom.hex }
let(:name) { SecureRandom.hex } let(:name) { SecureRandom.hex }
let(:run_after_create) { false } let(:run_after_create) { false }
let(:dast_profile) { Dast::Profile.find_by(project: project, name: name) } let(:dast_profile) { Dast::Profile.find_by(project: project, name: name) }
let(:dast_profile_schedule) { nil } let(:dast_profile_schedule) { nil }
...@@ -40,10 +39,16 @@ RSpec.describe Mutations::Dast::Profiles::Create do ...@@ -40,10 +39,16 @@ RSpec.describe Mutations::Dast::Profiles::Create do
context 'when the feature is licensed' do context 'when the feature is licensed' do
context 'when the user can run a dast scan' do context 'when the user can run a dast scan' do
let(:dast_profile_schedule) { attributes_for(:dast_profile_schedule) }
it 'returns the dast_profile' do it 'returns the dast_profile' do
expect(subject[:dast_profile]).to eq(dast_profile) expect(subject[:dast_profile]).to eq(dast_profile)
end end
it 'returns the dast_profile_schedule' do
expect(subject[:dast_profile_schedule]).to eq(dast_profile.dast_profile_schedule)
end
context 'when run_after_create=true' do context 'when run_after_create=true' do
let(:run_after_create) { true } let(:run_after_create) { true }
...@@ -54,30 +59,6 @@ RSpec.describe Mutations::Dast::Profiles::Create do ...@@ -54,30 +59,6 @@ RSpec.describe Mutations::Dast::Profiles::Create do
let(:delegated_params) { hash_including(dast_profile: instance_of(Dast::Profile)) } let(:delegated_params) { hash_including(dast_profile: instance_of(Dast::Profile)) }
end end
end end
context 'when dast_on_demand_scans_scheduler feature is enabled' do
let(:dast_profile_schedule) { attributes_for(:dast_profile_schedule) }
before do
stub_feature_flags(dast_on_demand_scans_scheduler: true)
end
it 'returns the dast_profile_schedule' do
expect(subject[:dast_profile_schedule]).to eq(dast_profile.dast_profile_schedule)
end
end
context 'when dast_on_demand_scans_scheduler feature is disabled' do
let(:dast_profile_schedule) { attributes_for(:dast_profile_schedule) }
before do
stub_feature_flags(dast_on_demand_scans_scheduler: false)
end
it 'returns the dast_profile_schedule' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end end
end end
end end
......
...@@ -96,7 +96,6 @@ RSpec.describe Mutations::Dast::Profiles::Update do ...@@ -96,7 +96,6 @@ RSpec.describe Mutations::Dast::Profiles::Update do
mutation.resolve(**params.merge(dast_profile_schedule: new_dast_profile_schedule)) mutation.resolve(**params.merge(dast_profile_schedule: new_dast_profile_schedule))
end end
context 'when dast_on_demand_scans_scheduler feature is enabled' do
it 'updates the profile schedule' do it 'updates the profile schedule' do
subject subject
...@@ -110,21 +109,7 @@ RSpec.describe Mutations::Dast::Profiles::Update do ...@@ -110,21 +109,7 @@ RSpec.describe Mutations::Dast::Profiles::Update do
end end
end end
context 'when dast_on_demand_scans_scheduler feature is disabled' do
let(:dast_profile_schedule_attrs) { attributes_for(:dast_profile_schedule) }
before do
stub_feature_flags(dast_on_demand_scans_scheduler: false)
end
it 'returns the dast_profile_schedule' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'when dast_profile_schedule param is not passed' do context 'when dast_profile_schedule param is not passed' do
context 'when dast_on_demand_scans_scheduler feature is enabled' do
it 'does not updates the profile schedule' do it 'does not updates the profile schedule' do
schedule_before_update = dast_profile.dast_profile_schedule schedule_before_update = dast_profile.dast_profile_schedule
...@@ -134,7 +119,6 @@ RSpec.describe Mutations::Dast::Profiles::Update do ...@@ -134,7 +119,6 @@ RSpec.describe Mutations::Dast::Profiles::Update do
end end
end end
end end
end
context 'when run_after_update=true' do context 'when run_after_update=true' do
let(:run_after_update) { true } let(:run_after_update) { true }
......
...@@ -37,20 +37,8 @@ RSpec.describe GitlabSchema.types['DastProfile'] do ...@@ -37,20 +37,8 @@ RSpec.describe GitlabSchema.types['DastProfile'] do
end end
describe 'dastProfileSchedule field' do describe 'dastProfileSchedule field' do
context 'when the feature flag is enabled' do
it 'correctly resolves the field' do it 'correctly resolves the field' do
expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to eq(object.dast_profile_schedule) expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to eq(object.dast_profile_schedule)
end end
end end
context 'when the feature flag is not enabled' do
before do
stub_feature_flags(dast_on_demand_scans_scheduler: false)
end
it 'is nil' do
expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to be_nil
end
end
end
end end
...@@ -27,18 +27,6 @@ RSpec.describe AppSec::Dast::ProfileScheduleWorker do ...@@ -27,18 +27,6 @@ RSpec.describe AppSec::Dast::ProfileScheduleWorker do
describe '#perform' do describe '#perform' do
subject { worker.perform } subject { worker.perform }
context 'when feature flag is disabled' do
before do
stub_feature_flags(dast_on_demand_scans_scheduler: false)
end
it 'does not call runnable_schedules' do
expect(::Dast::ProfileSchedule).not_to receive(:runnable_schedules)
subject
end
end
context 'when feature is licensed' do context 'when feature is licensed' do
before do before do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
......
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