Commit eb51df96 authored by Doug Stull's avatar Doug Stull

Merge branch 'pedropombeiro/297382/remove-variable_inside_variable-FF' into 'master'

Remove variable_inside_variable feature flag

See merge request gitlab-org/gitlab!73662
parents 3105d07e 9e60bd9a
...@@ -33,11 +33,7 @@ module Ci ...@@ -33,11 +33,7 @@ module Ci
end end
def runner_variables def runner_variables
if Feature.enabled?(:variable_inside_variable, project, default_enabled: :yaml) variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables
variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables
else
variables.to_runner_variables
end
end end
def refspecs def refspecs
......
---
name: variable_inside_variable
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50156
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297382
milestone: '13.11'
type: development
group: group::runner
default_enabled: true
...@@ -63,14 +63,12 @@ because the expansion is done in GitLab before any runner gets the job. ...@@ -63,14 +63,12 @@ because the expansion is done in GitLab before any runner gets the job.
#### Nested variable expansion #### Nested variable expansion
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48627) in GitLab 13.10. [Deployed behind the `variable_inside_variable` feature flag](../../user/feature_flags.md), disabled by default. - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48627) in GitLab 13.10. [Deployed behind the `variable_inside_variable` feature flag](../../user/feature_flags.md), disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/297382) in GitLab 14.3. - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/297382) in GitLab 14.3.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/297382) in GitLab 14.4. - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/297382) in GitLab 14.4.
- Feature flag `variable_inside_variable` removed in GitLab 14.5.
FLAG: GitLab expands job variable values recursively before sending them to the runner. For example, in the following scenario:
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 `variable_inside_variable`. On GitLab.com, this feature is available.
GitLab expands job variable values recursively before sending them to the runner. For example:
```yaml ```yaml
- BUILD_ROOT_DIR: '${CI_BUILDS_DIR}' - BUILD_ROOT_DIR: '${CI_BUILDS_DIR}'
...@@ -78,10 +76,7 @@ GitLab expands job variable values recursively before sending them to the runner ...@@ -78,10 +76,7 @@ GitLab expands job variable values recursively before sending them to the runner
- PACKAGE_PATH: '${OUT_PATH}/pkg' - PACKAGE_PATH: '${OUT_PATH}/pkg'
``` ```
If nested variable expansion is: The runner receives a valid, fully-formed path. For example, if `${CI_BUILDS_DIR}` is `/output`, then `PACKAGE_PATH` would be `/output/out/pkg`.
- **Disabled**: the runner receives `${BUILD_ROOT_DIR}/out/pkg`. This is not a valid path.
- **Enabled**: the runner receives a valid, fully-formed path. For example, if `${CI_BUILDS_DIR}` is `/output`, then `PACKAGE_PATH` would be `/output/out/pkg`.
References to unavailable variables are left intact. In this case, the runner References to unavailable variables are left intact. In this case, the runner
[attempts to expand the variable value](#gitlab-runner-internal-variable-expansion-mechanism) at runtime. [attempts to expand the variable value](#gitlab-runner-internal-variable-expansion-mechanism) at runtime.
......
...@@ -90,8 +90,6 @@ module Gitlab ...@@ -90,8 +90,6 @@ module Gitlab
end end
def sort_and_expand_all(project, keep_undefined: false) def sort_and_expand_all(project, keep_undefined: false)
return self if Feature.disabled?(:variable_inside_variable, project, default_enabled: :yaml)
sorted = Sort.new(self) sorted = Sort.new(self)
return self.class.new(self, sorted.errors) unless sorted.valid? return self.class.new(self, sorted.errors) unless sorted.valid?
......
...@@ -1218,14 +1218,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -1218,14 +1218,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
] ]
end end
context 'when FF :variable_inside_variable is enabled' do it "does not have errors" do
before do expect(subject.errors).to be_empty
stub_feature_flags(variable_inside_variable: [project])
end
it "does not have errors" do
expect(subject.errors).to be_empty
end
end end
end end
...@@ -1238,36 +1232,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -1238,36 +1232,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
] ]
end end
context 'when FF :variable_inside_variable is disabled' do it "returns an error" do
before do expect(subject.errors).to contain_exactly(
stub_feature_flags(variable_inside_variable: false) 'rspec: circular variable reference detected: ["A", "B", "C"]')
end
it "does not have errors" do
expect(subject.errors).to be_empty
end
end end
context 'when FF :variable_inside_variable is enabled' do context 'with job:rules:[if:]' do
before do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
stub_feature_flags(variable_inside_variable: [project])
end
it "returns an error" do it "included? does not raise" do
expect(subject.errors).to contain_exactly( expect { subject.included? }.not_to raise_error
'rspec: circular variable reference detected: ["A", "B", "C"]')
end end
context 'with job:rules:[if:]' do it "included? returns true" do
let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } expect(subject.included?).to eq(true)
it "included? does not raise" do
expect { subject.included? }.not_to raise_error
end
it "included? returns true" do
expect(subject.included?).to eq(true)
end
end end
end end
end end
......
...@@ -358,302 +358,212 @@ RSpec.describe Gitlab::Ci::Variables::Collection do ...@@ -358,302 +358,212 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
end end
describe '#sort_and_expand_all' do describe '#sort_and_expand_all' do
context 'when FF :variable_inside_variable is disabled' do let_it_be(:project) { create(:project) }
let_it_be(:project_with_flag_disabled) { create(:project) }
let_it_be(:project_with_flag_enabled) { create(:project) }
before do context 'table tests' do
stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) using RSpec::Parameterized::TableSyntax
end
context 'table tests' do where do
using RSpec::Parameterized::TableSyntax {
"empty array": {
where do variables: [],
{ keep_undefined: false,
"empty array": { result: []
variables: [], },
keep_undefined: false "simple expansions": {
}, variables: [
"simple expansions": { { key: 'variable', value: 'value' },
variables: [ { key: 'variable2', value: 'result' },
{ key: 'variable', value: 'value' }, { key: 'variable3', value: 'key$variable$variable2' },
{ key: 'variable2', value: 'result' }, { key: 'variable4', value: 'key$variable$variable3' }
{ key: 'variable3', value: 'key$variable$variable2' } ],
], keep_undefined: false,
keep_undefined: false result: [
}, { key: 'variable', value: 'value' },
"complex expansion": { { key: 'variable2', value: 'result' },
variables: [ { key: 'variable3', value: 'keyvalueresult' },
{ key: 'variable', value: 'value' }, { key: 'variable4', value: 'keyvaluekeyvalueresult' }
{ key: 'variable2', value: 'key${variable}' } ]
], },
keep_undefined: false "complex expansion": {
}, variables: [
"out-of-order variable reference": { { key: 'variable', value: 'value' },
variables: [ { key: 'variable2', value: 'key${variable}' }
{ key: 'variable2', value: 'key${variable}' }, ],
{ key: 'variable', value: 'value' } keep_undefined: false,
], result: [
keep_undefined: false { key: 'variable', value: 'value' },
}, { key: 'variable2', value: 'keyvalue' }
"complex expansions with raw variable": { ]
variables: [ },
{ key: 'variable3', value: 'key_${variable}_${variable2}' }, "unused variables": {
{ key: 'variable', value: '$variable2', raw: true }, variables: [
{ key: 'variable2', value: 'value2' } { key: 'variable', value: 'value' },
], { key: 'variable2', value: 'result2' },
keep_undefined: false { key: 'variable3', value: 'result3' },
}, { key: 'variable4', value: 'key$variable$variable3' }
"escaped characters in complex expansions are kept intact": { ],
variables: [ keep_undefined: false,
{ key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' }, result: [
{ key: 'variable', value: '$variable2' }, { key: 'variable', value: 'value' },
{ key: 'variable2', value: 'value2' } { key: 'variable2', value: 'result2' },
], { key: 'variable3', value: 'result3' },
keep_undefined: false { key: 'variable4', value: 'keyvalueresult3' }
}, ]
"array with cyclic dependency": { },
variables: [ "complex expansions": {
{ key: 'variable', value: '$variable2' }, variables: [
{ key: 'variable2', value: '$variable3' }, { key: 'variable', value: 'value' },
{ key: 'variable3', value: 'key$variable$variable2' } { key: 'variable2', value: 'result' },
], { key: 'variable3', value: 'key${variable}${variable2}' }
keep_undefined: true ],
} keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyvalueresult' }
]
},
"escaped characters in complex expansions keeping undefined are kept intact": {
variables: [
{ key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: 'value' }
],
keep_undefined: true,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'value' },
{ key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' }
]
},
"escaped characters in complex expansions discarding undefined are kept intact": {
variables: [
{ key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: 'value_$${HOME}_%%HOME%%' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value_$${HOME}_%%HOME%%' },
{ key: 'variable2', value: 'key__$${HOME}_%%HOME%%' }
]
},
"out-of-order expansion": {
variables: [
{ key: 'variable3', value: 'key$variable2$variable' },
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
],
keep_undefined: false,
result: [
{ key: 'variable2', value: 'result' },
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"out-of-order complex expansion": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key${variable2}${variable}' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"missing variable discarding original": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
keep_undefined: false,
result: [
{ key: 'variable2', value: 'key' }
]
},
"missing variable keeping original": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
keep_undefined: true,
result: [
{ key: 'variable2', value: 'key$variable' }
]
},
"complex expansions with missing variable keeping original": {
variables: [
{ key: 'variable4', value: 'key${variable}${variable2}${variable3}' },
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'value3' }
],
keep_undefined: true,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'value3' },
{ key: 'variable4', value: 'keyvalue${variable2}value3' }
]
},
"complex expansions with raw variable": {
variables: [
{ key: 'variable3', value: 'key_${variable}_${variable2}' },
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' },
{ key: 'variable3', value: 'key_$variable2_value2' }
]
},
"variable value referencing password with special characters": {
variables: [
{ key: 'VAR', value: '$PASSWORD' },
{ key: 'PASSWORD', value: 'my_password$$_%%_$A' },
{ key: 'A', value: 'value' }
],
keep_undefined: false,
result: [
{ key: 'VAR', value: 'my_password$$_%%_value' },
{ key: 'PASSWORD', value: 'my_password$$_%%_value' },
{ key: 'A', value: 'value' }
]
},
"cyclic dependency causes original array to be returned": {
variables: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
]
} }
end }
with_them do
let(:collection) { Gitlab::Ci::Variables::Collection.new(variables, keep_undefined: keep_undefined) }
subject { collection.sort_and_expand_all(project_with_flag_disabled) }
it 'returns Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
end
it 'does not expand variables' do
var_hash = variables.pluck(:key, :value).to_h
expect(subject.to_hash).to eq(var_hash)
end
end
end end
end
context 'when FF :variable_inside_variable is enabled' do with_them do
let_it_be(:project_with_flag_disabled) { create(:project) } let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
let_it_be(:project_with_flag_enabled) { create(:project) }
before do subject { collection.sort_and_expand_all(project, keep_undefined: keep_undefined) }
stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
context 'table tests' do it 'returns Collection' do
using RSpec::Parameterized::TableSyntax is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
where do
{
"empty array": {
variables: [],
keep_undefined: false,
result: []
},
"simple expansions": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key$variable$variable2' },
{ key: 'variable4', value: 'key$variable$variable3' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyvalueresult' },
{ key: 'variable4', value: 'keyvaluekeyvalueresult' }
]
},
"complex expansion": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'key${variable}' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'keyvalue' }
]
},
"unused variables": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result2' },
{ key: 'variable3', value: 'result3' },
{ key: 'variable4', value: 'key$variable$variable3' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result2' },
{ key: 'variable3', value: 'result3' },
{ key: 'variable4', value: 'keyvalueresult3' }
]
},
"complex expansions": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key${variable}${variable2}' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyvalueresult' }
]
},
"escaped characters in complex expansions keeping undefined are kept intact": {
variables: [
{ key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: 'value' }
],
keep_undefined: true,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'value' },
{ key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' }
]
},
"escaped characters in complex expansions discarding undefined are kept intact": {
variables: [
{ key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: 'value_$${HOME}_%%HOME%%' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value_$${HOME}_%%HOME%%' },
{ key: 'variable2', value: 'key__$${HOME}_%%HOME%%' }
]
},
"out-of-order expansion": {
variables: [
{ key: 'variable3', value: 'key$variable2$variable' },
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
],
keep_undefined: false,
result: [
{ key: 'variable2', value: 'result' },
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"out-of-order complex expansion": {
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'key${variable2}${variable}' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"missing variable discarding original": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
keep_undefined: false,
result: [
{ key: 'variable2', value: 'key' }
]
},
"missing variable keeping original": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
keep_undefined: true,
result: [
{ key: 'variable2', value: 'key$variable' }
]
},
"complex expansions with missing variable keeping original": {
variables: [
{ key: 'variable4', value: 'key${variable}${variable2}${variable3}' },
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'value3' }
],
keep_undefined: true,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable3', value: 'value3' },
{ key: 'variable4', value: 'keyvalue${variable2}value3' }
]
},
"complex expansions with raw variable": {
variables: [
{ key: 'variable3', value: 'key_${variable}_${variable2}' },
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: '$variable2', raw: true },
{ key: 'variable2', value: 'value2' },
{ key: 'variable3', value: 'key_$variable2_value2' }
]
},
"variable value referencing password with special characters": {
variables: [
{ key: 'VAR', value: '$PASSWORD' },
{ key: 'PASSWORD', value: 'my_password$$_%%_$A' },
{ key: 'A', value: 'value' }
],
keep_undefined: false,
result: [
{ key: 'VAR', value: 'my_password$$_%%_value' },
{ key: 'PASSWORD', value: 'my_password$$_%%_value' },
{ key: 'A', value: 'value' }
]
},
"cyclic dependency causes original array to be returned": {
variables: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: '$variable3' },
{ key: 'variable3', value: 'key$variable$variable2' }
]
}
}
end end
with_them do it 'expands variables' do
let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] }
.with_indifferent_access
subject { collection.sort_and_expand_all(project_with_flag_enabled, keep_undefined: keep_undefined) } expect(subject.to_hash).to eq(var_hash)
end
it 'returns Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
end
it 'expands variables' do
var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] }
.with_indifferent_access
expect(subject.to_hash).to eq(var_hash)
end
it 'preserves raw attribute' do it 'preserves raw attribute' do
expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h) expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h)
end
end end
end end
end end
......
...@@ -259,12 +259,7 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -259,12 +259,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
describe '#runner_variables' do describe '#runner_variables' do
subject { presenter.runner_variables } subject { presenter.runner_variables }
let_it_be(:project_with_flag_disabled) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_with_flag_enabled) { create(:project, :repository) }
before do
stub_feature_flags(variable_inside_variable: [project_with_flag_enabled])
end
shared_examples 'returns an array with the expected variables' do shared_examples 'returns an array with the expected variables' do
it 'returns an array' do it 'returns an array' do
...@@ -276,21 +271,11 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -276,21 +271,11 @@ RSpec.describe Ci::BuildRunnerPresenter do
end end
end end
context 'when FF :variable_inside_variable is disabled' do let(:sha) { project.repository.commit.sha }
let(:sha) { project_with_flag_disabled.repository.commit.sha } let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project_with_flag_disabled) } let(:build) { create(:ci_build, pipeline: pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
it_behaves_like 'returns an array with the expected variables'
end
context 'when FF :variable_inside_variable is enabled' do it_behaves_like 'returns an array with the expected variables'
let(:sha) { project_with_flag_enabled.repository.commit.sha }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: project_with_flag_enabled) }
let(:build) { create(:ci_build, pipeline: pipeline) }
it_behaves_like 'returns an array with the expected variables'
end
end end
describe '#runner_variables subset' do describe '#runner_variables subset' do
...@@ -305,32 +290,12 @@ RSpec.describe Ci::BuildRunnerPresenter do ...@@ -305,32 +290,12 @@ RSpec.describe Ci::BuildRunnerPresenter do
create(:ci_pipeline_variable, key: 'C', value: 'value', pipeline: build.pipeline) create(:ci_pipeline_variable, key: 'C', value: 'value', pipeline: build.pipeline)
end end
context 'when FF :variable_inside_variable is disabled' do it 'returns expanded and sorted variables' do
before do is_expected.to eq [
stub_feature_flags(variable_inside_variable: false) { key: 'C', value: 'value', public: false, masked: false },
end { key: 'B', value: 'refB-value-$D', public: false, masked: false },
{ key: 'A', value: 'refA-refB-value-$D', public: false, masked: false }
it 'returns non-expanded variables' do ]
is_expected.to eq [
{ key: 'A', value: 'refA-$B', public: false, masked: false },
{ key: 'B', value: 'refB-$C-$D', public: false, masked: false },
{ key: 'C', value: 'value', public: false, masked: false }
]
end
end
context 'when FF :variable_inside_variable is enabled' do
before do
stub_feature_flags(variable_inside_variable: [build.project])
end
it 'returns expanded and sorted variables' do
is_expected.to eq [
{ key: 'C', value: 'value', public: false, masked: false },
{ key: 'B', value: 'refB-value-$D', public: false, masked: false },
{ key: 'A', value: 'refA-refB-value-$D', public: false, masked: false }
]
end
end 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