Commit ea79ef49 authored by Fabio Pitino's avatar Fabio Pitino Committed by Shinya Maeda

Refactor YamlProcessor to return a result object

Simplifies UX of YamlProcessor to be better used for CI Lint.
Returns an object that can present the evaluated configuration,
any possible errors and warnings.
parent 2d363a19
...@@ -89,8 +89,6 @@ module EE ...@@ -89,8 +89,6 @@ module EE
def self.sast_ci_configuration(project) def self.sast_ci_configuration(project)
::Security::CiConfiguration::SastParserService.new(project).configuration ::Security::CiConfiguration::SastParserService.new(project).configuration
rescue ::Gitlab::Ci::YamlProcessor::ValidationError => ex
raise ::GraphQL::ExecutionError, ex.message
end end
end end
end end
......
...@@ -66,7 +66,10 @@ module Security ...@@ -66,7 +66,10 @@ module Security
def build_sast_attributes(content) def build_sast_attributes(content)
options = { project: @project, user: current_user, sha: @project.repository.commit.sha } options = { project: @project, user: current_user, sha: @project.repository.commit.sha }
sast_attributes = Gitlab::Ci::YamlProcessor.new(content, options).build_attributes(:sast) yaml_result = Gitlab::Ci::YamlProcessor.new(content, options).execute
return {} unless yaml_result.valid?
sast_attributes = yaml_result.build_attributes(:sast)
extract_required_attributes(sast_attributes) extract_required_attributes(sast_attributes)
end end
......
...@@ -25,7 +25,6 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -25,7 +25,6 @@ RSpec.describe GitlabSchema.types['Project'] do
describe 'sast_ci_configuration' do describe 'sast_ci_configuration' do
include_context 'read ci configuration for sast enabled project' include_context 'read ci configuration for sast enabled project'
let(:error_message) { "This is an error for YamlProcessor." }
let_it_be(:query) do let_it_be(:query) do
%( %(
...@@ -110,14 +109,6 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -110,14 +109,6 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(analyzer['label']).to eq('Brakeman') expect(analyzer['label']).to eq('Brakeman')
expect(analyzer['enabled']).to eq(true) expect(analyzer['enabled']).to eq(true)
end end
it 'returns an error if there is an exception in YamlProcessor' do
allow_next_instance_of(::Security::CiConfiguration::SastParserService) do |service|
allow(service).to receive(:configuration).and_raise(::Gitlab::Ci::YamlProcessor::ValidationError.new(error_message))
end
expect(subject["errors"].first["message"]).to eql(error_message)
end
end end
describe 'security_scanners' do describe 'security_scanners' do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe "CI YML Templates" do RSpec.describe "CI YML Templates" do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
subject { Gitlab::Ci::YamlProcessor.new(content) } subject { Gitlab::Ci::YamlProcessor.new(content).execute }
where(:template_name) do where(:template_name) do
Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name)
...@@ -24,7 +24,7 @@ RSpec.describe "CI YML Templates" do ...@@ -24,7 +24,7 @@ RSpec.describe "CI YML Templates" do
end end
it 'is valid' do it 'is valid' do
expect { subject }.not_to raise_error expect(subject).to be_valid
end end
it 'require default stages to be included' do it 'require default stages to be included' do
......
...@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
} }
end end
subject { described_class.new(YAML.dump(config)) } subject { described_class.new(YAML.dump(config)).execute }
context 'needs upstream pipeline' do context 'needs upstream pipeline' do
let(:needs) { { pipeline: 'some/project' } } let(:needs) { { pipeline: 'some/project' } }
...@@ -174,9 +174,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -174,9 +174,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
end end
it 'returns errors' do it 'returns errors' do
expect { subject } expect(subject.errors).to include(
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:bridge config should contain either a trigger or a needs:pipeline')
'jobs:bridge config should contain either a trigger or a needs:pipeline')
end end
end end
...@@ -198,9 +197,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -198,9 +197,8 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
end end
it 'returns errors' do it 'returns errors' do
expect { subject } expect(subject.errors).to contain_exactly(
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test:needs:need ref should be a string')
'jobs:test:needs:need ref should be a string')
end end
end end
end end
...@@ -216,10 +214,10 @@ RSpec.describe Gitlab::Ci::YamlProcessor do ...@@ -216,10 +214,10 @@ RSpec.describe Gitlab::Ci::YamlProcessor do
let(:config) { { deploy_to_production: { stage: 'deploy', script: ['echo'], secrets: secrets } } } let(:config) { { deploy_to_production: { stage: 'deploy', script: ['echo'], secrets: secrets } } }
subject(:processor) { described_class.new(YAML.dump(config)) } subject(:result) { described_class.new(YAML.dump(config)).execute }
it "returns secrets info" do it "returns secrets info" do
secrets = processor.stage_builds_attributes('deploy').first.fetch(:secrets) secrets = result.stage_builds_attributes('deploy').first.fetch(:secrets)
expect(secrets).to eq({ expect(secrets).to eq({
DATABASE_PASSWORD: { DATABASE_PASSWORD: {
......
...@@ -62,6 +62,10 @@ module Gitlab ...@@ -62,6 +62,10 @@ module Gitlab
root.jobs_value root.jobs_value
end end
def normalized_jobs
@normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs
end
private private
def expand_config(config) def expand_config(config)
......
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
end end
def normalize_jobs def normalize_jobs
return {} unless @jobs_config
return @jobs_config if parallelized_jobs.empty? return @jobs_config if parallelized_jobs.empty?
expand_parallelize_jobs do |job_name, config| expand_parallelize_jobs do |job_name, config|
......
...@@ -45,14 +45,15 @@ module Gitlab ...@@ -45,14 +45,15 @@ module Gitlab
end end
def static_validation(content) def static_validation(content)
result = Gitlab::Ci::YamlProcessor.new_with_validation_errors( result = Gitlab::Ci::YamlProcessor.new(
content, content,
project: @project, project: @project,
user: @current_user, user: @current_user,
sha: @project.repository.commit.sha) sha: @project.repository.commit.sha
).execute
Result.new( Result.new(
jobs: static_validation_convert_to_jobs(result.config&.stages, result.config&.builds), jobs: static_validation_convert_to_jobs(result),
errors: result.errors, errors: result.errors,
warnings: result.warnings warnings: result.warnings
) )
...@@ -76,12 +77,12 @@ module Gitlab ...@@ -76,12 +77,12 @@ module Gitlab
end end
end end
def static_validation_convert_to_jobs(stages, all_jobs) def static_validation_convert_to_jobs(result)
jobs = [] jobs = []
return jobs unless stages || all_jobs return jobs unless result.valid?
stages.each do |stage_name| result.stages.each do |stage_name|
all_jobs.each do |job| result.builds.each do |job|
next unless job[:stage] == stage_name next unless job[:stage] == stage_name
jobs << { jobs << {
......
...@@ -11,20 +11,23 @@ module Gitlab ...@@ -11,20 +11,23 @@ module Gitlab
def perform! def perform!
raise ArgumentError, 'missing config content' unless @command.config_content raise ArgumentError, 'missing config content' unless @command.config_content
@command.config_processor = ::Gitlab::Ci::YamlProcessor.new( result = ::Gitlab::Ci::YamlProcessor.new(
@command.config_content, { @command.config_content, {
project: project, project: project,
sha: @pipeline.sha, sha: @pipeline.sha,
user: current_user, user: current_user,
parent_pipeline: parent_pipeline parent_pipeline: parent_pipeline
} }
) ).execute
add_warnings_to_pipeline(result.warnings)
add_warnings_to_pipeline(@command.config_processor.warnings) if result.valid?
rescue Gitlab::Ci::YamlProcessor::ValidationError => ex @command.config_processor = result
add_warnings_to_pipeline(ex.warnings) else
error(result.errors.first, config_error: true)
end
error(ex.message, config_error: true)
rescue => ex rescue => ex
Gitlab::ErrorTracking.track_exception(ex, Gitlab::ErrorTracking.track_exception(ex,
project_id: project.id, project_id: project.id,
......
# frozen_string_literal: true # frozen_string_literal: true
# This is the CI Linter component that runs the syntax validations
# while parsing the YAML config into a data structure that is
# then presented to the caller as result object.
# After syntax validations (done by Ci::Config), this component also
# runs logical validation on the built data structure.
module Gitlab module Gitlab
module Ci module Ci
class YamlProcessor class YamlProcessor
# ValidationError is treated like a result object in the form of an exception. ValidationError = Class.new(StandardError)
# We can return any warnings, raised during the config validation, along with
# the error object until we support multiple messages to be returned.
class ValidationError < StandardError
attr_reader :warnings
def initialize(message, warnings: [])
@warnings = warnings
super(message)
end
end
include Gitlab::Config::Entry::LegacyValidationHelpers
attr_reader :stages, :jobs def self.validation_message(content, opts = {})
result = new(content, opts).execute
class Result result.errors.first
attr_reader :config, :errors, :warnings end
def initialize(config: nil, errors: [], warnings: []) def initialize(config_content, opts = {})
@config = config @config_content = config_content
@errors = errors @opts = opts
@warnings = warnings end
end
def valid? def execute
config.present? && errors.empty? if @config_content.blank?
return Result.new(errors: ['Please provide content of .gitlab-ci.yml'])
end end
end
def initialize(config, opts = {}) @ci_config = Gitlab::Ci::Config.new(@config_content, **@opts)
@ci_config = Gitlab::Ci::Config.new(config, **opts)
@config = @ci_config.to_hash
unless @ci_config.valid? unless @ci_config.valid?
error!(@ci_config.errors.first) return Result.new(ci_config: @ci_config, errors: @ci_config.errors, warnings: @ci_config.warnings)
end end
initial_parsing run_logical_validations!
rescue Gitlab::Ci::Config::ConfigError => e
error!(e.message)
end
def self.new_with_validation_errors(content, opts = {})
return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) if content.blank?
config = Gitlab::Ci::Config.new(content, **opts) Result.new(ci_config: @ci_config, warnings: @ci_config&.warnings)
return Result.new(errors: config.errors, warnings: config.warnings) unless config.valid?
config = Gitlab::Ci::YamlProcessor.new(content, opts)
Result.new(config: config, warnings: config.warnings)
rescue ValidationError => e
Result.new(errors: [e.message], warnings: e.warnings)
rescue Gitlab::Ci::Config::ConfigError => e rescue Gitlab::Ci::Config::ConfigError => e
Result.new(errors: [e.message]) Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings)
end
def warnings
@ci_config&.warnings || []
end
def builds
@jobs.map do |name, _|
build_attributes(name)
end
end
def build_attributes(name)
job = @jobs.fetch(name.to_sym, {})
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
tag_list: job[:tags],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: transform_to_yaml_variables(job[:variables]),
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
only: job[:only],
except: job[:except],
rules: job[:rules],
cache: job[:cache],
resource_group_key: job[:resource_group],
scheduling_type: job[:scheduling_type],
secrets: job[:secrets],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
cross_dependencies: job.dig(:needs, :cross_dependency),
job_timeout: job[:timeout],
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry],
parallel: job[:parallel],
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first,
release: release(job)
}.compact }.compact
end
def release(job) rescue ValidationError => e
job[:release] Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings)
end
def stage_builds_attributes(stage)
@jobs.values
.select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
end
def stages_attributes
@stages.uniq.map do |stage|
seeds = stage_builds_attributes(stage)
{ name: stage, index: @stages.index(stage), builds: seeds }
end
end
def workflow_attributes
{
rules: @config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(@variables)
}
end
def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
Gitlab::Ci::YamlProcessor.new(content, opts)
nil
rescue ValidationError => e
e.message
end
end end
private private
def initial_parsing def run_logical_validations!
##
# Global config
#
@variables = @ci_config.variables
@stages = @ci_config.stages @stages = @ci_config.stages
@jobs = @ci_config.normalized_jobs
##
# Jobs
#
@jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
@jobs.each do |name, job| @jobs.each do |name, job|
# logical validation for job validate_job!(name, job)
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
validate_job_needs!(name, job)
validate_dynamic_child_pipeline_dependencies!(name, job)
validate_job_environment!(name, job)
end end
end end
def transform_to_yaml_variables(variables) def validate_job!(name, job)
variables.to_h.map do |key, value| validate_job_stage!(name, job)
{ key: key.to_s, value: value, public: true } validate_job_dependencies!(name, job)
end validate_job_needs!(name, job)
validate_dynamic_child_pipeline_dependencies!(name, job)
validate_job_environment!(name, job)
end end
def validate_job_stage!(name, job) def validate_job_stage!(name, job)
...@@ -188,10 +70,6 @@ module Gitlab ...@@ -188,10 +70,6 @@ module Gitlab
end end
end end
def error!(message)
raise ValidationError.new(message, warnings: warnings)
end
def validate_job_dependencies!(name, job) def validate_job_dependencies!(name, job)
return unless job[:dependencies] return unless job[:dependencies]
...@@ -267,6 +145,10 @@ module Gitlab ...@@ -267,6 +145,10 @@ module Gitlab
error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined") error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined")
end end
end end
def error!(message)
raise ValidationError.new(message)
end
end end
end end
end end
# frozen_string_literal: true
# A data object that wraps `Ci::Config` and any messages
# (errors, warnings) generated by the YamlProcessor.
module Gitlab
module Ci
class YamlProcessor
class Result
attr_reader :errors, :warnings
def initialize(ci_config: nil, errors: [], warnings: [])
@ci_config = ci_config
@errors = errors || []
@warnings = warnings || []
end
def valid?
errors.empty?
end
def stages_attributes
stages.uniq.map do |stage|
seeds = stage_builds_attributes(stage)
{ name: stage, index: stages.index(stage), builds: seeds }
end
end
def builds
jobs.map do |name, _|
build_attributes(name)
end
end
def stage_builds_attributes(stage)
jobs.values
.select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
end
def workflow_attributes
{
rules: hash_config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(variables)
}
end
def jobs
@jobs ||= @ci_config.normalized_jobs
end
def stages
@stages ||= @ci_config.stages
end
def build_attributes(name)
job = jobs.fetch(name.to_sym, {})
{ stage_idx: stages.index(job[:stage]),
stage: job[:stage],
tag_list: job[:tags],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: transform_to_yaml_variables(job[:variables]),
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
only: job[:only],
except: job[:except],
rules: job[:rules],
cache: job[:cache],
resource_group_key: job[:resource_group],
scheduling_type: job[:scheduling_type],
secrets: job[:secrets],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
dependencies: job[:dependencies],
cross_dependencies: job.dig(:needs, :cross_dependency),
job_timeout: job[:timeout],
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry],
parallel: job[:parallel],
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
bridge_needs: job.dig(:needs, :bridge)&.first,
release: release(job)
}.compact }.compact
end
private
def variables
@variables ||= @ci_config.variables
end
def hash_config
@hash_config ||= @ci_config.to_hash
end
def release(job)
job[:release]
end
def transform_to_yaml_variables(variables)
variables.to_h.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
end
end
end
end
end
...@@ -98,7 +98,7 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -98,7 +98,7 @@ RSpec.describe Projects::Ci::LintsController do
it_behaves_like 'returns a successful validation' it_behaves_like 'returns a successful validation'
it 'runs validations through YamlProcessor' do it 'runs validations through YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original
subject subject
end end
...@@ -126,7 +126,7 @@ RSpec.describe Projects::Ci::LintsController do ...@@ -126,7 +126,7 @@ RSpec.describe Projects::Ci::LintsController do
it_behaves_like 'returns a successful validation' it_behaves_like 'returns a successful validation'
it 'runs validations through YamlProcessor' do it 'runs validations through YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original
subject subject
end end
......
...@@ -264,5 +264,11 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do ...@@ -264,5 +264,11 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do
is_expected.to match(config) is_expected.to match(config)
end end
end end
context 'when jobs config is nil' do
let(:config) { nil }
it { is_expected.to eq({}) }
end
end end
end end
...@@ -312,7 +312,7 @@ RSpec.describe Gitlab::Ci::Config do ...@@ -312,7 +312,7 @@ RSpec.describe Gitlab::Ci::Config do
HEREDOC HEREDOC
end end
it 'raises error YamlProcessor validationError' do it 'raises ConfigError' do
expect { config }.to raise_error( expect { config }.to raise_error(
described_class::ConfigError, described_class::ConfigError,
"Included file `invalid` does not have YAML extension!" "Included file `invalid` does not have YAML extension!"
...@@ -329,7 +329,7 @@ RSpec.describe Gitlab::Ci::Config do ...@@ -329,7 +329,7 @@ RSpec.describe Gitlab::Ci::Config do
HEREDOC HEREDOC
end end
it 'raises error YamlProcessor validationError' do it 'raises ConfigError' do
expect { config }.to raise_error( expect { config }.to raise_error(
described_class::ConfigError, described_class::ConfigError,
'Include `{"remote":"http://url","local":"/local/file.yml"}` needs to match exactly one accessor!' 'Include `{"remote":"http://url","local":"/local/file.yml"}` needs to match exactly one accessor!'
......
...@@ -157,7 +157,7 @@ RSpec.describe Gitlab::Ci::Lint do ...@@ -157,7 +157,7 @@ RSpec.describe Gitlab::Ci::Lint do
it 'uses YamlProcessor' do it 'uses YamlProcessor' do
expect(Gitlab::Ci::YamlProcessor) expect(Gitlab::Ci::YamlProcessor)
.to receive(:new_with_validation_errors) .to receive(:new)
.and_call_original .and_call_original
subject subject
......
...@@ -31,20 +31,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do ...@@ -31,20 +31,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
CI_YAML CI_YAML
end end
let(:yaml_processor) do let(:yaml_processor_result) do
::Gitlab::Ci::YamlProcessor.new( ::Gitlab::Ci::YamlProcessor.new(
ci_yaml, { ci_yaml, {
project: project, project: project,
sha: pipeline.sha, sha: pipeline.sha,
user: user user: user
} }
) ).execute
end end
let(:save_incompleted) { true } let(:save_incompleted) { true }
let(:command) do let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new( Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, config_processor: yaml_processor, save_incompleted: save_incompleted project: project, current_user: user, config_processor: yaml_processor_result, save_incompleted: save_incompleted
) )
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'CI YML Templates' do RSpec.describe 'CI YML Templates' do
subject { Gitlab::Ci::YamlProcessor.new(content) } subject { Gitlab::Ci::YamlProcessor.new(content).execute }
let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) } let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) }
...@@ -33,7 +33,7 @@ RSpec.describe 'CI YML Templates' do ...@@ -33,7 +33,7 @@ RSpec.describe 'CI YML Templates' do
end end
it 'is valid' do it 'is valid' do
expect { subject }.not_to raise_error expect(subject).to be_valid
end end
it 'require default stages to be included' do it 'require default stages to be included' do
......
This diff is collapsed.
...@@ -133,12 +133,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do ...@@ -133,12 +133,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do
expect(builds_count).to eq(1) expect(builds_count).to eq(1)
end end
it 'has no when YML attributes but only the DB column' do
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
subject
end
it 'has pipeline commits' do it 'has pipeline commits' do
expect(subject['ci_pipelines']).not_to be_empty expect(subject['ci_pipelines']).not_to be_empty
end end
......
...@@ -381,12 +381,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do ...@@ -381,12 +381,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do
expect(project_tree_saver.save).to be true expect(project_tree_saver.save).to be true
end end
it 'has no when YML attributes but only the DB column' do
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
project_tree_saver.save
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