config_kit 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +62 -0
- data/README.md +146 -0
- data/bin/ck +11 -0
- data/cluster/consul.yml +64 -0
- data/config/README.md +134 -0
- data/config/int0/int0.axle.yml +48 -0
- data/config/int0/int0.infra.yml +31 -0
- data/config/stg0/stg0.axle.yml +48 -0
- data/config/stg0/stg0.infra.yml +31 -0
- data/config_kit.gemspec +35 -0
- data/lib/config_kit.rb +20 -0
- data/lib/config_kit/cli/command.rb +77 -0
- data/lib/config_kit/cli/commands/bootstrap.rb +65 -0
- data/lib/config_kit/cli/commands/deploy.rb +48 -0
- data/lib/config_kit/cli/commands/describe.rb +41 -0
- data/lib/config_kit/cli/commands/get.rb +37 -0
- data/lib/config_kit/cli/commands/init.rb +42 -0
- data/lib/config_kit/cli/commands/rollback.rb +40 -0
- data/lib/config_kit/client.rb +203 -0
- data/lib/config_kit/config_data.rb +92 -0
- data/lib/config_kit/configuration.rb +16 -0
- data/lib/config_kit/data/loader.rb +91 -0
- data/lib/config_kit/data/loaders/file_loader.rb +68 -0
- data/lib/config_kit/data/loaders/git_loader.rb +39 -0
- data/lib/config_kit/deploy_data.rb +78 -0
- data/lib/config_kit/error.rb +3 -0
- data/lib/config_kit/ext/hash.rb +6 -0
- data/lib/config_kit/ext/slashed_hash.rb +45 -0
- data/lib/config_kit/idc_data.rb +28 -0
- data/lib/config_kit/manager.rb +104 -0
- data/lib/config_kit/tool.rb +168 -0
- data/lib/config_kit/version.rb +3 -0
- data/scripts/create_config_kit.rb +56 -0
- data/scripts/profile_to_consul.sh +9 -0
- metadata +139 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
module ConfigKit::Data
|
3
|
+
class FileLoader < Loader
|
4
|
+
def self.loader; "file"; end
|
5
|
+
|
6
|
+
attr_reader :files
|
7
|
+
def initialize(uri_kls, env, app, branch)
|
8
|
+
@uri_kls, @env, @app, @branch = uri_kls, env, app, branch
|
9
|
+
@path = File.expand_path('.',File.join(retrieve_path(@uri_kls.path),env))
|
10
|
+
super()
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(&block)
|
14
|
+
return run_all unless block
|
15
|
+
run_batch(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_all
|
19
|
+
files_data = {}
|
20
|
+
@files.each do |f|
|
21
|
+
files_data.merge!(load_one(f))
|
22
|
+
end
|
23
|
+
files_data
|
24
|
+
end
|
25
|
+
|
26
|
+
def run_batch(&block)
|
27
|
+
while !finish?
|
28
|
+
next_batch
|
29
|
+
files_data = {}
|
30
|
+
@current_files.each do |f|
|
31
|
+
files_data.merge!(load_one(f))
|
32
|
+
end
|
33
|
+
block.call(files_data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def retrieve_files
|
38
|
+
files = if @app == 'all'
|
39
|
+
Dir["#{@path}/**/*.yml"].select { |f| match_for?(f, @env) }
|
40
|
+
else
|
41
|
+
Dir["#{@path}/**/*.yml"].select {|f| match_for?(f, @app) && match_for?(f, @env)}
|
42
|
+
end
|
43
|
+
raise ConfigKit::Data::Loader::LoaderFailure.new('No data file found.') if files.empty?
|
44
|
+
files
|
45
|
+
end
|
46
|
+
private
|
47
|
+
|
48
|
+
def retrieve_path(path)
|
49
|
+
split_path = path[1..-1].split('/')
|
50
|
+
return path[1..-1] if split_path[0] == '.'
|
51
|
+
path
|
52
|
+
end
|
53
|
+
|
54
|
+
def env_app_for(f)
|
55
|
+
File.basename(f).split('.')[0..1]
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_one(f)
|
59
|
+
env, app = env_app_for(f)
|
60
|
+
raise ConfigKit::Data::Loader::LoaderFailure.new("Wrong data file env(#{env}) for loaded #{@env}") unless env == @env
|
61
|
+
{app => YAML.load_file(f)}
|
62
|
+
end
|
63
|
+
|
64
|
+
def match_for?(f,info)
|
65
|
+
File.basename(f).split('.').find {|a| a == info}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'git'
|
2
|
+
module ConfigKit::Data
|
3
|
+
class GitLoader < Loader
|
4
|
+
class GitLoaderError < ConfigKit::Error; end
|
5
|
+
def self.loader; "git"; end
|
6
|
+
attr_reader :clone_path
|
7
|
+
def initialize(uri_kls, env, app, version)
|
8
|
+
@uri_kls, @env, @app, @version = uri_kls, env, app, version
|
9
|
+
@clone_path = Dir.mktmpdir
|
10
|
+
@file_path = "file://#{@clone_path}"
|
11
|
+
@file_kls = URI.parse(@file_path)
|
12
|
+
clone
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(&block)
|
16
|
+
ConfigKit.logger.debug "Git is loading env(#{@env}) #{@app} from #{@uri_kls.to_s}"
|
17
|
+
begin
|
18
|
+
FileLoader.new(@file_kls, @env, @app, @version).run(&block)
|
19
|
+
ensure
|
20
|
+
FileUtils.rm_rf @clone_path
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def clone
|
26
|
+
ConfigKit.logger.debug "Git is cloning env(#{@env}) #{@app} from #{@uri_kls.to_s}"
|
27
|
+
begin
|
28
|
+
g = Git.clone(@uri_kls.to_s, @clone_path)
|
29
|
+
tags = g.tags.map { |t| t.name }
|
30
|
+
raise GitLoaderError.new "Version(#{@version}) not found" unless tags.include?(@version)
|
31
|
+
g.checkout(@version)
|
32
|
+
rescue GitLoaderError => e
|
33
|
+
raise GitLoaderError.new e.message
|
34
|
+
rescue Exception => e
|
35
|
+
raise GitLoaderError.new "Unknown error to load (#{@version}) in #{@uri_kls.to_s}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'config_kit/ext/hash'
|
2
|
+
module ConfigKit
|
3
|
+
class DeployData < SlashedHash
|
4
|
+
class DeployDataOptsError < ConfigKit::Error; end
|
5
|
+
def initialize(name, version, opts)
|
6
|
+
@name = name
|
7
|
+
@version = version[0] == 'v' ? version : "v#{version}"
|
8
|
+
@api_version = opts.fetch(:api_version, '1.0')
|
9
|
+
@kind = opts.fetch(:kind, 'config_kit')
|
10
|
+
@extra = opts.fetch(:extra, :no_default)
|
11
|
+
@cs = opts.fetch(:cs, "no_cs") if @extra != :default_only
|
12
|
+
|
13
|
+
ConfigKit.logger.debug "Deploy in #{@extra} options"
|
14
|
+
check_extra(@extra)
|
15
|
+
@binded_data = binding_data
|
16
|
+
super(@binded_data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def binding_deploy
|
20
|
+
{'deploy' => data}
|
21
|
+
end
|
22
|
+
|
23
|
+
def binding_kind
|
24
|
+
{ @kind => binding_deploy}
|
25
|
+
end
|
26
|
+
|
27
|
+
def binding_default
|
28
|
+
{
|
29
|
+
@kind => {
|
30
|
+
'deploy' => deploy_default_data
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def binding_data
|
36
|
+
if @extra == :no_default
|
37
|
+
binding_kind
|
38
|
+
elsif @extra == :set_default
|
39
|
+
binding_default.deep_merge(binding_kind)
|
40
|
+
elsif @extra == :default_only
|
41
|
+
binding_default
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def data
|
46
|
+
{
|
47
|
+
@name => {
|
48
|
+
@version =>
|
49
|
+
{
|
50
|
+
"ts" => ts,
|
51
|
+
"cs" => @cs
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def deploy_default_data
|
58
|
+
{
|
59
|
+
@name => {
|
60
|
+
'default' => @version
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def extra_options
|
67
|
+
[:no_default, :set_default, :default_only]
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_extra(extra)
|
71
|
+
raise DeployDataOptsError.new "extra options(#{extra_options.join('|')}) error: #{extra}." unless extra_options.include?(extra)
|
72
|
+
end
|
73
|
+
|
74
|
+
def ts
|
75
|
+
(Time.now.to_f * 1000).to_i
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ConfigKit
|
2
|
+
class SlashedHash < ::Hash
|
3
|
+
class SlashedHashTypeError < Exception; end
|
4
|
+
def initialize(hash, keep_nesting=false)
|
5
|
+
self.merge!(hash) if keep_nesting
|
6
|
+
|
7
|
+
self.merge!(dot_flattened(hash))
|
8
|
+
SlashedHash.symbolize(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def inspect
|
12
|
+
"#<#{self.class.name}:#{object_id} #{super}>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
{}.replace(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.symbolize(hash)
|
20
|
+
hash.keys.each do |key|
|
21
|
+
hash[key.to_sym] = hash.delete(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
# turns {'a' => {'b' => 'c'}} into {'a.b' => 'c'}
|
27
|
+
def dot_flattened(nested_hash, names=[], result={})
|
28
|
+
nested_hash.each do |key, val|
|
29
|
+
next if val == nil
|
30
|
+
if val.respond_to?(:has_key?)
|
31
|
+
dot_flattened(val, names + [key], result)
|
32
|
+
elsif val.is_a?(Array)
|
33
|
+
result[(names + [key]).join('/')] = (val.map do |e|
|
34
|
+
raise SlashedHashTypeError.new "Not Support #{e.class.name} in array" unless e.respond_to?(:to_s)
|
35
|
+
e.to_s
|
36
|
+
end).join(',')
|
37
|
+
else
|
38
|
+
result[(names + [key]).join('/')] = val
|
39
|
+
end
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'config_kit/ext/slashed_hash'
|
2
|
+
|
3
|
+
module ConfigKit
|
4
|
+
class IDCData < SlashedHash
|
5
|
+
def initialize(name, env, opts)
|
6
|
+
@name, @env = name, env
|
7
|
+
@api_version = opts.fetch(:api_version, '1.0')
|
8
|
+
@kind = opts.fetch(:kind, 'config_kit')
|
9
|
+
@bind_data = binding_data
|
10
|
+
super(@bind_data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def data
|
14
|
+
{
|
15
|
+
'name' => @name,
|
16
|
+
'env' => @env
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def binding_idc
|
21
|
+
{ 'idc' => data}
|
22
|
+
end
|
23
|
+
|
24
|
+
def binding_data
|
25
|
+
{ @kind => binding_idc }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'config_kit/tool'
|
3
|
+
module ConfigKit
|
4
|
+
class Manager
|
5
|
+
class IDCEnvMissing < ConfigKit::Error; end
|
6
|
+
def self.bootstrap(app, from, uri_kls, version, opts={})
|
7
|
+
opts['uri_kls'] = uri_kls
|
8
|
+
opts['version'] = version
|
9
|
+
opts['from'] = from
|
10
|
+
new(app, opts).bootstrap
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.describe(app, version, opts={})
|
14
|
+
opts['version'] = version
|
15
|
+
new(app, opts).describe
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.rollback(app, version, opts={})
|
19
|
+
opts['version'] = version
|
20
|
+
new(app, opts).rollback
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.deploy(app, version, opts={})
|
24
|
+
opts['version'] = version
|
25
|
+
new(app, opts).deploy
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.init(name, env='int0', opts={})
|
29
|
+
app = 'idc'
|
30
|
+
opts['name'] = name
|
31
|
+
opts['env'] = env
|
32
|
+
opts['skip_env_check'] = true
|
33
|
+
new(app,opts).init
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.get(app,opts={})
|
37
|
+
new(app, opts).get
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(app, opts)
|
41
|
+
@app = app
|
42
|
+
@opts = opts
|
43
|
+
@tool = ConfigKit::Tool.new
|
44
|
+
unless opts['skip_env_check'] == true
|
45
|
+
env = @tool.get_idc_env
|
46
|
+
raise ConfigKit::Manager::IDCEnvMissing.new 'IDC environment missing, pls init it first!' if env.nil?
|
47
|
+
@opts['env'] = env
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def init
|
53
|
+
name = @opts.delete('name')
|
54
|
+
env = @opts.delete('env')
|
55
|
+
@tool.init_txn
|
56
|
+
@tool.idc_init_txn(name, env, @opts)
|
57
|
+
@tool.perform_txn
|
58
|
+
end
|
59
|
+
|
60
|
+
def describe
|
61
|
+
version = @opts['version']
|
62
|
+
@tool.describe(@app, version)
|
63
|
+
end
|
64
|
+
|
65
|
+
def bootstrap
|
66
|
+
create
|
67
|
+
{app: @app, version: @opts['version']}
|
68
|
+
end
|
69
|
+
|
70
|
+
def create(extra=:no_default)
|
71
|
+
ConfigKit::Data::Loader.load(@app, @opts['from'], @opts['uri_kls'], @opts['env'], @opts['version']) do |data|
|
72
|
+
@tool.init_txn
|
73
|
+
data.each_pair do |k,v|
|
74
|
+
@tool.bootstrap_txn(v, k)
|
75
|
+
version, cs = get_deploy_info(@tool.config_data)
|
76
|
+
@tool.deploy_txn(k, version, extra,cs)
|
77
|
+
end
|
78
|
+
@tool.perform_txn
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_deploy_info(config_data)
|
83
|
+
return [] unless config_data.kind_of?(ConfigKit::ConfigData)
|
84
|
+
[config_data.data_version, config_data.data_cs]
|
85
|
+
end
|
86
|
+
|
87
|
+
def get
|
88
|
+
data = @tool.get(@app)[@app]
|
89
|
+
data || {}
|
90
|
+
end
|
91
|
+
|
92
|
+
def deploy
|
93
|
+
change_default
|
94
|
+
end
|
95
|
+
|
96
|
+
def rollback
|
97
|
+
change_default
|
98
|
+
end
|
99
|
+
|
100
|
+
def change_default
|
101
|
+
@tool.deploy_txn(@app, @opts['version'], :default_only)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'config_kit/config_data'
|
2
|
+
require 'config_kit/deploy_data'
|
3
|
+
require 'config_kit/idc_data'
|
4
|
+
require 'config_kit/ext/hash'
|
5
|
+
module ConfigKit
|
6
|
+
class Tool
|
7
|
+
class ConfigMetadataError < ConfigKit::Error; end
|
8
|
+
class MissingDefaultVersionError < ConfigKit::Error;end
|
9
|
+
attr_reader :kind, :namespace, :api_version, :config_data, :deploy_data, :idc_data
|
10
|
+
def initialize(opts={}, api_version=ConfigKit.config.api_version, kind=ConfigKit.config.kind)
|
11
|
+
@kind = kind
|
12
|
+
@api_version = api_version
|
13
|
+
@url = opts.fetch(:url, ConfigKit.config.url)
|
14
|
+
@acl_token = opts.fetch(:acl_token, ConfigKit.config.acl_token)
|
15
|
+
@client = ConfigKit::Client.new(@url, @acl_token)
|
16
|
+
end
|
17
|
+
|
18
|
+
def bootstrap_config(data, name)
|
19
|
+
ConfigKit::ConfigData.new(data, name, api_version, kind)
|
20
|
+
end
|
21
|
+
|
22
|
+
def bootstrap_txn(data,name)
|
23
|
+
@config_data = bootstrap_config(data, name)
|
24
|
+
@client.create_txn(@config_data)
|
25
|
+
end
|
26
|
+
|
27
|
+
def deploy_config(name, version,extra,cs)
|
28
|
+
ConfigKit::DeployData.new(name, version, extra: extra, cs: cs)
|
29
|
+
end
|
30
|
+
|
31
|
+
def deploy_txn(app,version,extra,cs=nil)
|
32
|
+
if extra != :no_default && check_version_for?(app,version)
|
33
|
+
ConfigKit.logger.debug "Missing version(#{version}) for app(#{app}) to set default"
|
34
|
+
raise MissingDefaultVersionError.new "Missing default version for app(#{app}), pls set default version first.\n"
|
35
|
+
end
|
36
|
+
ConfigKit.logger.debug "compose deploy data for app(#{app}:#{version})"
|
37
|
+
@deploy_data = deploy_config(app,version,extra, cs)
|
38
|
+
@client.create_txn(@deploy_data)
|
39
|
+
@client.perform_txn
|
40
|
+
@deploy_data
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_version_for?(app, version)
|
44
|
+
path = path_for(app, version)
|
45
|
+
@client.read(path).nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def idc_config(name, env, opts)
|
49
|
+
ConfigKit::IDCData.new(name, env, opts)
|
50
|
+
end
|
51
|
+
|
52
|
+
def idc_init_txn(name, env, opts={})
|
53
|
+
@idc_data = idc_config(name, env, opts)
|
54
|
+
@client.create_txn(@idc_data)
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_idc
|
58
|
+
data = @client.read(idc_path)
|
59
|
+
return {'idc' => 'N/A'} if data.nil?
|
60
|
+
data['config_kit']['idc']
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_idc_env
|
64
|
+
data = @client.read(idc_path)
|
65
|
+
return nil if data.nil?
|
66
|
+
data['config_kit']['idc']['env']
|
67
|
+
end
|
68
|
+
|
69
|
+
def describe(app, version)
|
70
|
+
@content = {}
|
71
|
+
data = @client.read(idc_path)
|
72
|
+
return @content if data.nil?
|
73
|
+
@content['idc'] = data['config_kit']['idc']
|
74
|
+
data = @client.read(deploy_path)
|
75
|
+
return @content if data.nil?
|
76
|
+
@content['deploy'] = data['config_kit']['deploy']
|
77
|
+
unless app == 'idc'
|
78
|
+
if version.nil?
|
79
|
+
data = @client.read(path_for(app))
|
80
|
+
return @content if data.nil?
|
81
|
+
@content[app] = data['config_kit'][app]
|
82
|
+
else
|
83
|
+
data = @client.read(path_for(app))
|
84
|
+
return @content if data.nil?
|
85
|
+
@content[app] = data['config_kit'][app][version]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
@content
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete_txn(data, name, version=nil)
|
92
|
+
config = bootstrap_config(data)
|
93
|
+
end
|
94
|
+
|
95
|
+
def get(app)
|
96
|
+
return get_all if app == 'all'
|
97
|
+
get_one(app)
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_all()
|
101
|
+
config_data = {}
|
102
|
+
all_apps.each do |app|
|
103
|
+
config_data = config_data.deep_merge(get_one(app))
|
104
|
+
end
|
105
|
+
config_data
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_one(app)
|
109
|
+
context = {}
|
110
|
+
app_data = get_app(app)
|
111
|
+
context[app] = app_data
|
112
|
+
context[app]['idc'] = get_idc
|
113
|
+
unless app == 'infra'
|
114
|
+
infra_data = get_app('infra')['service']
|
115
|
+
context[app]['service'] = infra_data
|
116
|
+
end
|
117
|
+
context
|
118
|
+
end
|
119
|
+
|
120
|
+
def get_app(app)
|
121
|
+
version = version_for(app)
|
122
|
+
if version.nil?
|
123
|
+
ConfigKit.logger.debug "Missing default version for app(#{app})"
|
124
|
+
raise MissingDefaultVersionError.new "Missing default version for app(#{app}), pls set default version first.\n"
|
125
|
+
end
|
126
|
+
app_path = path_for(app, version)
|
127
|
+
pp app_path
|
128
|
+
pp @client.read(app_path)
|
129
|
+
app_version = @client.read(app_path)[kind]
|
130
|
+
app_data = {}
|
131
|
+
app_data[app] = app_version[app][version]
|
132
|
+
end
|
133
|
+
|
134
|
+
def all_apps
|
135
|
+
apps = @client.read(deploy_path)[kind]['deploy'].keys
|
136
|
+
apps
|
137
|
+
end
|
138
|
+
|
139
|
+
def version_for(app)
|
140
|
+
@client.read(deploy_app_path(app),false)
|
141
|
+
end
|
142
|
+
|
143
|
+
def init_txn
|
144
|
+
@client.init_txn
|
145
|
+
end
|
146
|
+
|
147
|
+
def perform_txn
|
148
|
+
@client.perform_txn
|
149
|
+
end
|
150
|
+
|
151
|
+
def deploy_app_path(app)
|
152
|
+
"#{deploy_path}/#{app}/default"
|
153
|
+
end
|
154
|
+
|
155
|
+
def deploy_path
|
156
|
+
"/#{kind}/deploy"
|
157
|
+
end
|
158
|
+
|
159
|
+
def path_for(app, version=nil)
|
160
|
+
return "/#{kind}/#{app}" if version.nil?
|
161
|
+
"/#{kind}/#{app}/#{version}"
|
162
|
+
end
|
163
|
+
|
164
|
+
def idc_path
|
165
|
+
"/#{kind}/idc"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|