kuber_kit 0.1.7 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -2
- data/README.md +16 -3
- data/TODO.md +4 -3
- data/example/configurations/review.rb +2 -1
- data/example/images/app_sources/Dockerfile +1 -1
- data/example/images/ruby_app/image.rb +4 -4
- data/example/infrastructure/build_servers.rb +8 -0
- data/example/services/env_file.rb +5 -1
- data/example/services/ruby_app.rb +2 -1
- data/kuber_kit.gemspec +1 -0
- data/lib/kuber_kit.rb +37 -19
- data/lib/kuber_kit/actions/configuration_loader.rb +11 -2
- data/lib/kuber_kit/actions/env_file_reader.rb +5 -0
- data/lib/kuber_kit/actions/image_compiler.rb +16 -10
- data/lib/kuber_kit/actions/kubectl_applier.rb +9 -3
- data/lib/kuber_kit/actions/kubectl_attacher.rb +26 -0
- data/lib/kuber_kit/actions/service_deployer.rb +34 -0
- data/lib/kuber_kit/actions/service_reader.rb +6 -0
- data/lib/kuber_kit/actions/template_reader.rb +5 -0
- data/lib/kuber_kit/cli.rb +54 -20
- data/lib/kuber_kit/configs.rb +24 -22
- data/lib/kuber_kit/container.rb +21 -13
- data/lib/kuber_kit/core/artifacts/artifact_store.rb +12 -23
- data/lib/kuber_kit/core/build_servers/abstract_build_server.rb +21 -0
- data/lib/kuber_kit/core/build_servers/build_server.rb +24 -0
- data/lib/kuber_kit/core/build_servers/build_server_store.rb +18 -0
- data/lib/kuber_kit/core/configuration.rb +10 -4
- data/lib/kuber_kit/core/configuration_definition.rb +18 -1
- data/lib/kuber_kit/core/configuration_factory.rb +11 -1
- data/lib/kuber_kit/core/configuration_store.rb +14 -24
- data/lib/kuber_kit/core/context_helper/service_helper.rb +2 -2
- data/lib/kuber_kit/core/env_files/env_file_store.rb +8 -23
- data/lib/kuber_kit/core/image_store.rb +8 -18
- data/lib/kuber_kit/core/registries/registry_store.rb +8 -23
- data/lib/kuber_kit/core/service.rb +6 -2
- data/lib/kuber_kit/core/service_store.rb +13 -23
- data/lib/kuber_kit/core/store.rb +48 -0
- data/lib/kuber_kit/core/templates/template_store.rb +12 -23
- data/lib/kuber_kit/image_compiler/build_server_pool.rb +31 -0
- data/lib/kuber_kit/image_compiler/build_server_pool_factory.rb +13 -0
- data/lib/kuber_kit/image_compiler/image_build_dir_creator.rb +13 -7
- data/lib/kuber_kit/image_compiler/image_dependency_resolver.rb +25 -5
- data/lib/kuber_kit/preprocessing/file_preprocessor.rb +5 -4
- data/lib/kuber_kit/service_deployer/strategies/kubernetes.rb +10 -3
- data/lib/kuber_kit/shell/abstract_shell.rb +4 -0
- data/lib/kuber_kit/shell/{bash_commands.rb → commands/bash_commands.rb} +1 -1
- data/lib/kuber_kit/shell/{docker_commands.rb → commands/docker_commands.rb} +1 -1
- data/lib/kuber_kit/shell/{git_commands.rb → commands/git_commands.rb} +1 -1
- data/lib/kuber_kit/shell/commands/kubectl_commands.rb +65 -0
- data/lib/kuber_kit/shell/commands/rsync_commands.rb +32 -0
- data/lib/kuber_kit/shell/local_shell.rb +24 -5
- data/lib/kuber_kit/shell/ssh_session.rb +62 -0
- data/lib/kuber_kit/shell/ssh_shell.rb +77 -0
- data/lib/kuber_kit/tools/file_presence_checker.rb +6 -2
- data/lib/kuber_kit/ui/interactive.rb +8 -0
- data/lib/kuber_kit/ui/simple.rb +6 -0
- data/lib/kuber_kit/version.rb +2 -2
- metadata +34 -12
- data/lib/kuber_kit/preprocessing/dir_preprocessor.rb +0 -19
- data/lib/kuber_kit/shell/kubectl_commands.rb +0 -42
- data/lib/kuber_kit/shell/rsync_commands.rb +0 -20
- data/lib/kuber_kit/tools/files_sync.rb +0 -10
@@ -6,6 +6,7 @@ class KuberKit::Core::ConfigurationFactory
|
|
6
6
|
"core.artifact_store",
|
7
7
|
"core.env_file_store",
|
8
8
|
"core.template_store",
|
9
|
+
"core.build_server_store",
|
9
10
|
"configs"
|
10
11
|
]
|
11
12
|
|
@@ -16,6 +17,7 @@ class KuberKit::Core::ConfigurationFactory
|
|
16
17
|
registries = fetch_registries(configuration_attrs.registries)
|
17
18
|
env_files = fetch_env_files(configuration_attrs.env_files)
|
18
19
|
templates = fetch_templates(configuration_attrs.templates)
|
20
|
+
build_servers = fetch_build_servers(configuration_attrs.build_servers)
|
19
21
|
|
20
22
|
KuberKit::Core::Configuration.new(
|
21
23
|
name: configuration_attrs.name,
|
@@ -25,7 +27,9 @@ class KuberKit::Core::ConfigurationFactory
|
|
25
27
|
templates: templates,
|
26
28
|
kubeconfig_path: configuration_attrs.kubeconfig_path,
|
27
29
|
deploy_strategy: configuration_attrs.deploy_strategy || configs.deploy_strategy,
|
28
|
-
|
30
|
+
deploy_namespace: configuration_attrs.deploy_namespace,
|
31
|
+
services_attributes: configuration_attrs.services_attributes,
|
32
|
+
build_servers: build_servers
|
29
33
|
)
|
30
34
|
end
|
31
35
|
|
@@ -61,4 +65,10 @@ class KuberKit::Core::ConfigurationFactory
|
|
61
65
|
end
|
62
66
|
result
|
63
67
|
end
|
68
|
+
|
69
|
+
def fetch_build_servers(build_servers)
|
70
|
+
build_servers.map do |build_server_name|
|
71
|
+
build_server_store.get(build_server_name)
|
72
|
+
end
|
73
|
+
end
|
64
74
|
end
|
@@ -1,7 +1,4 @@
|
|
1
1
|
class KuberKit::Core::ConfigurationStore
|
2
|
-
NotFoundError = Class.new(KuberKit::NotFoundError)
|
3
|
-
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
-
|
5
2
|
include KuberKit::Import[
|
6
3
|
"core.configuration_factory",
|
7
4
|
"core.configuration_definition_factory",
|
@@ -16,24 +13,12 @@ class KuberKit::Core::ConfigurationStore
|
|
16
13
|
end
|
17
14
|
|
18
15
|
def add_definition(configuration_definition)
|
19
|
-
|
20
|
-
|
21
|
-
unless @@configuration_definitions[configuration_definition.configuration_name].nil?
|
22
|
-
raise AlreadyAddedError, "image #{configuration_definition.configuration_name} was already added"
|
23
|
-
end
|
24
|
-
|
25
|
-
@@configuration_definitions[configuration_definition.configuration_name] = configuration_definition
|
16
|
+
definitions_store.add(configuration_definition.configuration_name, configuration_definition)
|
26
17
|
end
|
27
18
|
|
28
19
|
Contract Symbol => Any
|
29
20
|
def get_definition(configuration_name)
|
30
|
-
|
31
|
-
|
32
|
-
if @@configuration_definitions[configuration_name].nil?
|
33
|
-
raise NotFoundError, "configuration #{configuration_name} not found"
|
34
|
-
end
|
35
|
-
|
36
|
-
@@configuration_definitions[configuration_name]
|
21
|
+
definitions_store.get(configuration_name)
|
37
22
|
end
|
38
23
|
|
39
24
|
Contract Symbol => Any
|
@@ -57,18 +42,23 @@ class KuberKit::Core::ConfigurationStore
|
|
57
42
|
end
|
58
43
|
|
59
44
|
def reset!
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
def all_definitions
|
64
|
-
@@configuration_definitions ||= {}
|
45
|
+
definitions_store.reset!
|
65
46
|
end
|
66
47
|
|
67
48
|
def count
|
68
|
-
|
49
|
+
definitions_store.size
|
69
50
|
end
|
70
51
|
|
71
52
|
def exists?(configuration_name)
|
72
|
-
|
53
|
+
definitions_store.exists?(configuration_name)
|
73
54
|
end
|
55
|
+
|
56
|
+
def all_definitions
|
57
|
+
definitions_store.items
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def definitions_store
|
62
|
+
@@definitions_store ||= KuberKit::Core::Store.new(KuberKit::Core::ConfigurationDefinition)
|
63
|
+
end
|
74
64
|
end
|
@@ -17,7 +17,7 @@ class KuberKit::Core::ContextHelper::ServiceHelper < KuberKit::Core::ContextHelp
|
|
17
17
|
@service.uri
|
18
18
|
end
|
19
19
|
|
20
|
-
def attribute(attribute_name)
|
21
|
-
@service.attribute(attribute_name)
|
20
|
+
def attribute(attribute_name, default: nil)
|
21
|
+
@service.attribute(attribute_name, default: default)
|
22
22
|
end
|
23
23
|
end
|
@@ -1,19 +1,6 @@
|
|
1
1
|
class KuberKit::Core::EnvFiles::EnvFileStore
|
2
|
-
NotFoundError = Class.new(KuberKit::NotFoundError)
|
3
|
-
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
-
|
5
2
|
def add(env_file)
|
6
|
-
|
7
|
-
|
8
|
-
if !env_file.is_a?(KuberKit::Core::EnvFiles::AbstractEnvFile)
|
9
|
-
raise ArgumentError.new("should be an instance of KuberKit::Core::EnvFiles::AbstractEnvFile, got: #{env_file.inspect}")
|
10
|
-
end
|
11
|
-
|
12
|
-
unless @@env_files[env_file.name].nil?
|
13
|
-
raise AlreadyAddedError, "env_file #{env_file.name} was already added"
|
14
|
-
end
|
15
|
-
|
16
|
-
@@env_files[env_file.name] = env_file
|
3
|
+
store.add(env_file.name, env_file)
|
17
4
|
end
|
18
5
|
|
19
6
|
def get(env_file_name)
|
@@ -24,14 +11,7 @@ class KuberKit::Core::EnvFiles::EnvFileStore
|
|
24
11
|
end
|
25
12
|
|
26
13
|
def get_global(env_file_name)
|
27
|
-
|
28
|
-
env_file = @@env_files[env_file_name]
|
29
|
-
|
30
|
-
if env_file.nil?
|
31
|
-
raise NotFoundError, "env_file '#{env_file_name}' not found"
|
32
|
-
end
|
33
|
-
|
34
|
-
env_file
|
14
|
+
store.get(env_file_name)
|
35
15
|
end
|
36
16
|
|
37
17
|
def get_from_configuration(env_file_name)
|
@@ -40,6 +20,11 @@ class KuberKit::Core::EnvFiles::EnvFileStore
|
|
40
20
|
end
|
41
21
|
|
42
22
|
def reset!
|
43
|
-
|
23
|
+
store.reset!
|
44
24
|
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def store
|
28
|
+
@@store ||= KuberKit::Core::Store.new(KuberKit::Core::EnvFiles::AbstractEnvFile)
|
29
|
+
end
|
45
30
|
end
|
@@ -1,7 +1,4 @@
|
|
1
1
|
class KuberKit::Core::ImageStore
|
2
|
-
NotFoundError = Class.new(KuberKit::Error)
|
3
|
-
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
-
|
5
2
|
include KuberKit::Import[
|
6
3
|
"core.image_factory",
|
7
4
|
"core.image_definition_factory",
|
@@ -16,24 +13,12 @@ class KuberKit::Core::ImageStore
|
|
16
13
|
end
|
17
14
|
|
18
15
|
def add_definition(image_definition)
|
19
|
-
|
20
|
-
|
21
|
-
unless @@image_definitions[image_definition.image_name].nil?
|
22
|
-
raise AlreadyAddedError, "image #{image_definition.image_name} was already added"
|
23
|
-
end
|
24
|
-
|
25
|
-
@@image_definitions[image_definition.image_name] = image_definition
|
16
|
+
definitions_store.add(image_definition.image_name, image_definition)
|
26
17
|
end
|
27
18
|
|
28
19
|
Contract Symbol => Any
|
29
20
|
def get_definition(image_name)
|
30
|
-
|
31
|
-
|
32
|
-
if @@image_definitions[image_name].nil?
|
33
|
-
raise NotFoundError, "image #{image_name} not found"
|
34
|
-
end
|
35
|
-
|
36
|
-
@@image_definitions[image_name]
|
21
|
+
definitions_store.get(image_name)
|
37
22
|
end
|
38
23
|
|
39
24
|
Contract Symbol => Any
|
@@ -57,6 +42,11 @@ class KuberKit::Core::ImageStore
|
|
57
42
|
end
|
58
43
|
|
59
44
|
def reset!
|
60
|
-
|
45
|
+
definitions_store.reset!
|
61
46
|
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def definitions_store
|
50
|
+
@@definitions_store ||= KuberKit::Core::Store.new(KuberKit::Core::ImageDefinition)
|
51
|
+
end
|
62
52
|
end
|
@@ -1,19 +1,6 @@
|
|
1
1
|
class KuberKit::Core::Registries::RegistryStore
|
2
|
-
NotFoundError = Class.new(KuberKit::NotFoundError)
|
3
|
-
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
-
|
5
2
|
def add(registry)
|
6
|
-
|
7
|
-
|
8
|
-
if !registry.is_a?(KuberKit::Core::Registries::AbstractRegistry)
|
9
|
-
raise ArgumentError.new("should be an instance of KuberKit::Core::Registries::AbstractRegistry, got: #{registry.inspect}")
|
10
|
-
end
|
11
|
-
|
12
|
-
unless @@registries[registry.name].nil?
|
13
|
-
raise AlreadyAddedError, "registry #{registry.name} was already added"
|
14
|
-
end
|
15
|
-
|
16
|
-
@@registries[registry.name] = registry
|
3
|
+
store.add(registry.name, registry)
|
17
4
|
end
|
18
5
|
|
19
6
|
def get(registry_name)
|
@@ -24,14 +11,7 @@ class KuberKit::Core::Registries::RegistryStore
|
|
24
11
|
end
|
25
12
|
|
26
13
|
def get_global(registry_name)
|
27
|
-
|
28
|
-
registry = @@registries[registry_name]
|
29
|
-
|
30
|
-
if registry.nil?
|
31
|
-
raise NotFoundError, "registry '#{registry_name}' not found"
|
32
|
-
end
|
33
|
-
|
34
|
-
registry
|
14
|
+
store.get(registry_name)
|
35
15
|
end
|
36
16
|
|
37
17
|
def get_from_configuration(registry_name)
|
@@ -44,6 +24,11 @@ class KuberKit::Core::Registries::RegistryStore
|
|
44
24
|
end
|
45
25
|
|
46
26
|
def reset!
|
47
|
-
|
27
|
+
store.reset!
|
48
28
|
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def store
|
32
|
+
@@store ||= KuberKit::Core::Store.new(KuberKit::Core::Registries::AbstractRegistry)
|
33
|
+
end
|
49
34
|
end
|
@@ -22,11 +22,15 @@ class KuberKit::Core::Service
|
|
22
22
|
name.to_s.gsub("_", "-")
|
23
23
|
end
|
24
24
|
|
25
|
-
def attribute(attribute_name)
|
26
|
-
|
25
|
+
def attribute(attribute_name, default: nil)
|
26
|
+
if !attributes.has_key?(attribute_name.to_sym) && default.nil?
|
27
27
|
raise AttributeNotSet, "attribute #{attribute_name} was not set"
|
28
28
|
end
|
29
29
|
|
30
|
+
if !attributes.has_key?(attribute_name.to_sym) && !default.nil?
|
31
|
+
return default
|
32
|
+
end
|
33
|
+
|
30
34
|
attributes[attribute_name.to_sym]
|
31
35
|
end
|
32
36
|
end
|
@@ -1,7 +1,4 @@
|
|
1
1
|
class KuberKit::Core::ServiceStore
|
2
|
-
NotFoundError = Class.new(KuberKit::Error)
|
3
|
-
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
-
|
5
2
|
include KuberKit::Import[
|
6
3
|
"core.service_factory",
|
7
4
|
"core.service_definition_factory",
|
@@ -16,24 +13,12 @@ class KuberKit::Core::ServiceStore
|
|
16
13
|
end
|
17
14
|
|
18
15
|
def add_definition(service_definition)
|
19
|
-
|
20
|
-
|
21
|
-
unless @@service_definitions[service_definition.service_name].nil?
|
22
|
-
raise AlreadyAddedError, "service #{service_definition.service_name} was already added"
|
23
|
-
end
|
24
|
-
|
25
|
-
@@service_definitions[service_definition.service_name] = service_definition
|
16
|
+
definitions_store.add(service_definition.service_name, service_definition)
|
26
17
|
end
|
27
18
|
|
28
19
|
Contract Symbol => Any
|
29
20
|
def get_definition(service_name)
|
30
|
-
|
31
|
-
|
32
|
-
if @@service_definitions[service_name].nil?
|
33
|
-
raise NotFoundError, "service '#{service_name}' not found"
|
34
|
-
end
|
35
|
-
|
36
|
-
@@service_definitions[service_name]
|
21
|
+
definitions_store.get(service_name)
|
37
22
|
end
|
38
23
|
|
39
24
|
Contract Symbol => Any
|
@@ -57,18 +42,23 @@ class KuberKit::Core::ServiceStore
|
|
57
42
|
end
|
58
43
|
|
59
44
|
def reset!
|
60
|
-
|
45
|
+
definitions_store.reset!
|
61
46
|
end
|
62
47
|
|
63
|
-
def
|
64
|
-
|
48
|
+
def count
|
49
|
+
definitions_store.size
|
65
50
|
end
|
66
51
|
|
67
|
-
def
|
68
|
-
|
52
|
+
def all_definitions
|
53
|
+
definitions_store.items
|
69
54
|
end
|
70
55
|
|
71
56
|
def exists?(service_name)
|
72
|
-
|
57
|
+
definitions_store.exists?(service_name)
|
73
58
|
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def definitions_store
|
62
|
+
@@definitions_store ||= KuberKit::Core::Store.new(KuberKit::Core::ServiceDefinition)
|
63
|
+
end
|
74
64
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class KuberKit::Core::Store
|
2
|
+
NotFoundError = Class.new(KuberKit::NotFoundError)
|
3
|
+
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
+
|
5
|
+
attr_reader :object_class_name
|
6
|
+
|
7
|
+
def initialize(object_class_name)
|
8
|
+
@object_class_name = object_class_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(item_name, item)
|
12
|
+
unless item.is_a?(object_class_name)
|
13
|
+
raise ArgumentError.new("#{self.object_class_name}: should be an instance of #{object_class_name}, got: #{item.inspect}")
|
14
|
+
end
|
15
|
+
|
16
|
+
unless items[item_name].nil?
|
17
|
+
raise AlreadyAddedError, "#{self.object_class_name}: item with name #{item_name} was already added"
|
18
|
+
end
|
19
|
+
|
20
|
+
items[item_name] = item
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(item_name)
|
24
|
+
item = items[item_name]
|
25
|
+
|
26
|
+
if item.nil?
|
27
|
+
raise NotFoundError, "#{self.object_class_name}: item '#{item_name}' not found"
|
28
|
+
end
|
29
|
+
|
30
|
+
item
|
31
|
+
end
|
32
|
+
|
33
|
+
def items
|
34
|
+
@items ||= {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset!
|
38
|
+
@items = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def size
|
42
|
+
items.count
|
43
|
+
end
|
44
|
+
|
45
|
+
def exists?(name)
|
46
|
+
!items[name].nil?
|
47
|
+
end
|
48
|
+
end
|
@@ -1,19 +1,6 @@
|
|
1
1
|
class KuberKit::Core::Templates::TemplateStore
|
2
|
-
NotFoundError = Class.new(KuberKit::NotFoundError)
|
3
|
-
AlreadyAddedError = Class.new(KuberKit::Error)
|
4
|
-
|
5
2
|
def add(template)
|
6
|
-
|
7
|
-
|
8
|
-
if !template.is_a?(KuberKit::Core::Templates::AbstractTemplate)
|
9
|
-
raise ArgumentError.new("should be an instance of KuberKit::Core::Templates::AbstractTemplate, got: #{template.inspect}")
|
10
|
-
end
|
11
|
-
|
12
|
-
unless @@templates[template.name].nil?
|
13
|
-
raise AlreadyAddedError, "template #{template.name} was already added"
|
14
|
-
end
|
15
|
-
|
16
|
-
@@templates[template.name] = template
|
3
|
+
store.add(template.name, template)
|
17
4
|
end
|
18
5
|
|
19
6
|
def get(template_name)
|
@@ -24,14 +11,7 @@ class KuberKit::Core::Templates::TemplateStore
|
|
24
11
|
end
|
25
12
|
|
26
13
|
def get_global(template_name)
|
27
|
-
|
28
|
-
template = @@templates[template_name]
|
29
|
-
|
30
|
-
if template.nil?
|
31
|
-
raise NotFoundError, "template '#{template_name}' not found"
|
32
|
-
end
|
33
|
-
|
34
|
-
template
|
14
|
+
store.get(template_name)
|
35
15
|
end
|
36
16
|
|
37
17
|
def get_from_configuration(template_name)
|
@@ -40,6 +20,15 @@ class KuberKit::Core::Templates::TemplateStore
|
|
40
20
|
end
|
41
21
|
|
42
22
|
def reset!
|
43
|
-
|
23
|
+
store.reset!
|
24
|
+
end
|
25
|
+
|
26
|
+
def exists?(template_name)
|
27
|
+
store.exists?(template_name)
|
44
28
|
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def store
|
32
|
+
@@store ||= KuberKit::Core::Store.new(KuberKit::Core::Templates::AbstractTemplate)
|
33
|
+
end
|
45
34
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class KuberKit::ImageCompiler::BuildServerPool
|
2
|
+
attr_reader :ssh_shells, :local_shell
|
3
|
+
|
4
|
+
def initialize(local_shell:, build_servers:, ssh_shell_class:)
|
5
|
+
@local_shell = local_shell
|
6
|
+
@ssh_shell_class = ssh_shell_class
|
7
|
+
@build_servers = build_servers
|
8
|
+
@ssh_shells = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_shell
|
12
|
+
if @build_servers.any?
|
13
|
+
shell = connect_to_ssh_shell(@build_servers.sample)
|
14
|
+
@ssh_shells << shell
|
15
|
+
shell
|
16
|
+
else
|
17
|
+
@local_shell
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def disconnect_all
|
22
|
+
@ssh_shells.each(&:disconnect)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def connect_to_ssh_shell(bs)
|
27
|
+
shell = @ssh_shell_class.new
|
28
|
+
shell.connect(host: bs.host, user: bs.user, port: bs.port)
|
29
|
+
shell
|
30
|
+
end
|
31
|
+
end
|