Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle YAML configuration format on configuration file #3712

Merged
merged 15 commits into from May 18, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/fluent/command/fluentd.rb
Expand Up @@ -126,6 +126,18 @@
opts[:without_source] = b
}

op.on('--config-file-type VALU', 'file type of fluentd. yaml or yml') { |s|
if (s == 'yaml') || (s == 'yml')
opts[:config_file_type] = s.to_sym
else
usage '--config-file-type accepts yaml or yml'
end
}

op.on('--guess-config-file-type', "Guess config file type that is yaml or not (default)", TrueClass) {|b|
cosmo0920 marked this conversation as resolved.
Show resolved Hide resolved
opts[:guess_config_file_type] = b
}

op.on('--use-v1-config', "Use v1 configuration format (default)", TrueClass) {|b|
opts[:use_v1_config] = b
}
Expand Down
15 changes: 14 additions & 1 deletion lib/fluent/config.rb
Expand Up @@ -17,6 +17,7 @@
require 'fluent/config/error'
require 'fluent/config/element'
require 'fluent/configurable'
require 'fluent/config/yaml_parser'

module Fluent
module Config
Expand All @@ -25,7 +26,18 @@ module Config
# @param additional_config [String] config which is added to last of config body
# @param use_v1_config [Bool] config is formatted with v1 or not
# @return [Fluent::Config]
def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_config: true)
def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_config: true, type: nil, guess_type: nil)
if guess_type
config_fext = File.extname(config_path.dup)
cosmo0920 marked this conversation as resolved.
Show resolved Hide resolved
if config_fext == '.yaml' || config_fext == '.yml'
type = :yaml
end
end

if type == :yaml || type == :yml
return Fluent::Config::YamlParser.parse(config_path)
end

config_fname = File.basename(config_path)
config_basedir = File.dirname(config_path)
config_data = File.open(config_path, "r:#{encoding}:utf-8") do |f|
Expand All @@ -36,6 +48,7 @@ def self.build(config_path:, encoding: 'utf-8', additional_config: nil, use_v1_c
end
s
end

Fluent::Config.parse(config_data, config_fname, config_basedir, use_v1_config)
end

Expand Down
44 changes: 44 additions & 0 deletions lib/fluent/config/yaml_parser.rb
@@ -0,0 +1,44 @@
#
# Fluentd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'fluent/config/yaml_parser/loader'
require 'fluent/config/yaml_parser/parser'
require 'pathname'

module Fluent
module Config
module YamlParser
def self.parse(path)
context = Kernel.binding

unless context.respond_to?(:use_nil)
context.define_singleton_method(:use_nil) do
raise Fluent::SetNil
end
end

unless context.respond_to?(:use_default)
context.define_singleton_method(:use_default) do
raise Fluent::SetDefault
end
end

s = Fluent::Config::YamlParser::Loader.new(context).load(Pathname.new(path))
Fluent::Config::YamlParser::Parser.new(s).build.to_element
end
end
end
end
47 changes: 47 additions & 0 deletions lib/fluent/config/yaml_parser/fluent_value.rb
@@ -0,0 +1,47 @@
#
# Fluentd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

module Fluent
module Config
module YamlParser
module FluentValue
JsonValue = Struct.new(:val) do
def to_s
val.to_json
end

def to_element
to_s
end
end

StringValue = Struct.new(:val, :context) do
def to_s
context.instance_eval("\"#{val}\"")
rescue Fluent::SetNil => _
''
rescue Fluent::SetDefault => _
':default'
end

def to_element
to_s
end
end
end
end
end
end
91 changes: 91 additions & 0 deletions lib/fluent/config/yaml_parser/loader.rb
@@ -0,0 +1,91 @@
#
# Fluentd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'psych'
require 'json'
require 'fluent/config/error'
require 'fluent/config/yaml_parser/fluent_value'

# Based on https://github.com/eagletmt/hako/blob/34cdde06fe8f3aeafd794be830180c3cedfbb4dc/lib/hako/yaml_loader.rb

module Fluent
module Config
module YamlParser
class Loader
INCLUDE_TAG = 'tag:include'.freeze
FLUENT_JSON_TAG = 'tag:fluent/json'.freeze
FLUENT_STR_TAG = 'tag:fluent/s'.freeze
SHOVEL = '<<'.freeze

def initialize(context = Kernel.binding)
@context = context
@current_path = nil
end

# @param [String] path
# @return [Hash]
def load(path)
class_loader = Psych::ClassLoader.new
scanner = Psych::ScalarScanner.new(class_loader)

visitor = Visitor.new(scanner, class_loader)

visitor._register_domain(INCLUDE_TAG) do |_, val|
load(path.parent.join(val))
end

visitor._register_domain(FLUENT_JSON_TAG) do |_, val|
Fluent::Config::YamlParser::FluentValue::JsonValue.new(val)
end

visitor._register_domain(FLUENT_STR_TAG) do |_, val|
Fluent::Config::YamlParser::FluentValue::StringValue.new(val, @context)
end

path.open do |f|
visitor.accept(Psych.parse(f))
end
end

class Visitor < Psych::Visitors::ToRuby
def initialize(scanner, class_loader)
super(scanner, class_loader)
end

def _register_domain(name, &block)
@domain_types.merge!({ name => [name, block] })
end

def revive_hash(hash, o)
super(hash, o).tap do |r|
if r[SHOVEL].is_a?(Hash)
h2 = {}
r.each do |k, v|
if k == SHOVEL
h2.merge!(v)
else
h2[k] = v
end
end
r.replace(h2)
end
end
end
end
end
end
end
end
154 changes: 154 additions & 0 deletions lib/fluent/config/yaml_parser/parser.rb
@@ -0,0 +1,154 @@
#
# Fluentd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'fluent/config/yaml_parser/section_builder'

module Fluent
module Config
module YamlParser
class Parser
def initialize(config, indent: 2)
@base_indent = indent
@config = config
end

def build
s = @config['system'] && system_config_build(@config['system'])
c = @config['config'] && config_build(@config['config'], root: true)
ashie marked this conversation as resolved.
Show resolved Hide resolved
RootBuilder.new(s, c)
end

private

def system_config_build(config)
section_build('system', config)
end

def config_build(config, indent: 0, root: false)
sb = SectionBodyBuilder.new(indent, root: root)
config.each do |c|
if (lc = c.delete('label'))
sb.add_section(label_build(lc, indent: indent))
end

if (sc = c.delete('source'))
sb.add_section(source_build(sc, indent: indent))
end

if (fc = c.delete('filter'))
sb.add_section(filter_build(fc, indent: indent))
end

if (mc = c.delete('match'))
sb.add_section(match_build(mc, indent: indent))
end

if (wc = c.delete('worker'))
sb.add_section(worker_build(wc, indent: indent))
end

included_sections_build(c, sb, indent: indent)
end

sb
end

def label_build(config, indent: 0)
config = config.dup
name = config.delete('$name')
c = config.delete('config')
SectionBuilder.new('label', config_build(c, indent: indent + @base_indent), indent, name)
end

def worker_build(config, indent: 0)
config = config.dup
num = config.delete('$arg')
c = config.delete('config')
SectionBuilder.new('worker', config_build(c, indent: indent + @base_indent), indent, num)
end

def source_build(config, indent: 0)
section_build('source', config, indent: indent)
end

def filter_build(config, indent: 0)
config = config.dup
tag = config.delete('$tag')
section_build('filter', config, indent: indent, arg: tag)
end

def match_build(config, indent: 0)
config = config.dup
tag = config.delete('$tag')
section_build('match', config, indent: indent, arg: tag)
end

def included_sections_build(config, section_builder, indent: 0)
config.each_entry do |e|
k = e.keys.first
cc = e.delete(k)
case k
when 'label'
section_builder.add_section(label_build(cc, indent: indent))
when 'worker'
section_builder.add_section(worker_build(cc, indent: indent))
when 'source'
section_builder.add_section(source_build(cc, indent: indent))
when 'filter'
section_builder.add_section(filter_build(cc, indent: indent))
when 'match'
section_builder.add_section(match_build(cc, indent: indent))
end
end
end

def section_build(name, config, indent: 0, arg: nil)
sb = SectionBodyBuilder.new(indent + @base_indent)

if (v = config.delete('$type'))
sb.add_line('@type', v)
end

if (v = config.delete('$label'))
sb.add_line('@label', v)
end

config.each do |key, val|
if val.is_a?(Array)
val.each do |v|
sb.add_section(section_build(key, v, indent: indent + @base_indent))
end
elsif val.is_a?(Hash)
harg = val.delete('$arg')
if harg.is_a?(Array)
# To prevent to generate invalid configuration for arg.
# "arg" should be String object and concatenated by ","
# when two or more objects are specified there.
sb.add_section(section_build(key, val, indent: indent + @base_indent, arg: harg&.join(',')))
else
sb.add_section(section_build(key, val, indent: indent + @base_indent, arg: harg))
end
else
sb.add_line(key, val)
end
end

SectionBuilder.new(name, sb, indent, arg)
end
end
end
end
end