kuber_kit 0.5.1 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Gemfile.lock +5 -5
  4. data/TODO.md +5 -5
  5. data/example/configurations/review.rb +1 -1
  6. data/example/services/docker_app.rb +2 -1
  7. data/example/services/env_file.rb +1 -1
  8. data/example/services/ruby_app.rb +2 -1
  9. data/lib/kuber_kit.rb +10 -0
  10. data/lib/kuber_kit/actions/kubectl_get.rb +32 -0
  11. data/lib/kuber_kit/actions/service_checker.rb +5 -0
  12. data/lib/kuber_kit/actions/service_deployer.rb +85 -61
  13. data/lib/kuber_kit/cli.rb +14 -3
  14. data/lib/kuber_kit/configs.rb +10 -6
  15. data/lib/kuber_kit/container.rb +20 -0
  16. data/lib/kuber_kit/core/artifacts/artifact_store.rb +3 -0
  17. data/lib/kuber_kit/core/configuration.rb +4 -2
  18. data/lib/kuber_kit/core/configuration_definition.rb +7 -0
  19. data/lib/kuber_kit/core/configuration_factory.rb +1 -0
  20. data/lib/kuber_kit/core/dependencies/abstract_dependency_resolver.rb +75 -0
  21. data/lib/kuber_kit/core/env_files/abstract_env_file.rb +4 -0
  22. data/lib/kuber_kit/core/env_files/artifact_file.rb +4 -0
  23. data/lib/kuber_kit/core/env_files/env_file_store.rb +3 -0
  24. data/lib/kuber_kit/core/env_files/env_group.rb +12 -0
  25. data/lib/kuber_kit/core/image.rb +2 -1
  26. data/lib/kuber_kit/core/registries/registry_store.rb +3 -0
  27. data/lib/kuber_kit/core/service.rb +4 -2
  28. data/lib/kuber_kit/core/service_definition.rb +13 -6
  29. data/lib/kuber_kit/core/service_factory.rb +1 -0
  30. data/lib/kuber_kit/core/templates/template_store.rb +3 -0
  31. data/lib/kuber_kit/env_file_reader/env_file_parser.rb +51 -0
  32. data/lib/kuber_kit/env_file_reader/env_file_tempfile_creator.rb +17 -0
  33. data/lib/kuber_kit/env_file_reader/reader.rb +2 -0
  34. data/lib/kuber_kit/env_file_reader/strategies/artifact_file.rb +9 -65
  35. data/lib/kuber_kit/env_file_reader/strategies/env_group.rb +21 -0
  36. data/lib/kuber_kit/image_compiler/image_dependency_resolver.rb +5 -53
  37. data/lib/kuber_kit/service_deployer/service_dependency_resolver.rb +14 -0
  38. data/lib/kuber_kit/service_deployer/service_list_resolver.rb +11 -6
  39. data/lib/kuber_kit/service_deployer/strategies/docker.rb +31 -12
  40. data/lib/kuber_kit/service_deployer/strategies/kubernetes.rb +9 -2
  41. data/lib/kuber_kit/shell/commands/kubectl_commands.rb +8 -0
  42. data/lib/kuber_kit/shell/local_shell.rb +15 -1
  43. data/lib/kuber_kit/template_reader/strategies/artifact_file.rb +3 -3
  44. data/lib/kuber_kit/tools/logger_factory.rb +14 -0
  45. data/lib/kuber_kit/version.rb +1 -1
  46. metadata +10 -2
@@ -4,8 +4,8 @@ class KuberKit::Configs
4
4
  AVAILABLE_CONFIGS = [
5
5
  :image_dockerfile_name, :image_build_context_dir, :image_tag, :docker_ignore_list, :image_compile_dir,
6
6
  :kuber_kit_dirname, :kuber_kit_min_version, :images_dirname, :services_dirname, :infra_dirname, :configurations_dirname,
7
- :artifact_clone_dir, :service_config_dir, :deployer_strategy, :compile_simultaneous_limit,
8
- :additional_images_paths, :deprecation_warnings_disabled, :log_file_path
7
+ :artifact_clone_dir, :service_config_dir, :deployer_strategy, :compile_simultaneous_limit, :deploy_simultaneous_limit,
8
+ :additional_images_paths, :deprecation_warnings_disabled, :log_file_path, :env_file_compile_dir
9
9
  ]
10
10
  DOCKER_IGNORE_LIST = [
11
11
  'Dockerfile',
@@ -34,10 +34,12 @@ class KuberKit::Configs
34
34
  end
35
35
 
36
36
  def add_default_configs
37
+ home_kuber_kit_path = File.expand_path(File.join("~", ".kuber_kit"))
38
+
37
39
  set :image_dockerfile_name, "Dockerfile"
38
40
  set :image_build_context_dir, "build_context"
39
41
  set :image_tag, 'latest'
40
- set :image_compile_dir, "/tmp/kuber_kit/image_builds"
42
+ set :image_compile_dir, File.join(home_kuber_kit_path, "image_builds")
41
43
  set :docker_ignore_list, DOCKER_IGNORE_LIST
42
44
  set :kuber_kit_dirname, "kuber_kit"
43
45
  set :kuber_kit_min_version, KuberKit::VERSION
@@ -45,13 +47,15 @@ class KuberKit::Configs
45
47
  set :services_dirname, "services"
46
48
  set :infra_dirname, "infrastructure"
47
49
  set :configurations_dirname, "configurations"
48
- set :artifact_clone_dir, "/tmp/kuber_kit/artifacts"
49
- set :service_config_dir, "/tmp/kuber_kit/services"
50
+ set :artifact_clone_dir, File.join(home_kuber_kit_path, "artifacts")
51
+ set :service_config_dir, File.join(home_kuber_kit_path, "services")
50
52
  set :deployer_strategy, :kubernetes
51
53
  set :compile_simultaneous_limit, 5
54
+ set :deploy_simultaneous_limit, 5
52
55
  set :additional_images_paths, []
53
56
  set :deprecation_warnings_disabled, false
54
- set :log_file_path, "/tmp/kuber_kit.log"
57
+ set :log_file_path, File.join(home_kuber_kit_path, "deploy.log")
58
+ set :env_file_compile_dir, File.join(home_kuber_kit_path, "env_files")
55
59
  end
56
60
 
57
61
  def items
@@ -45,6 +45,10 @@ class KuberKit::Container
45
45
  KuberKit::Actions::KubectlDescribe.new
46
46
  end
47
47
 
48
+ register "actions.kubectl_get" do
49
+ KuberKit::Actions::KubectlGet.new
50
+ end
51
+
48
52
  register "actions.kubectl_logs" do
49
53
  KuberKit::Actions::KubectlLogs.new
50
54
  end
@@ -225,10 +229,22 @@ class KuberKit::Container
225
229
  KuberKit::EnvFileReader::Reader.new
226
230
  end
227
231
 
232
+ register "env_file_reader.env_file_parser" do
233
+ KuberKit::EnvFileReader::EnvFileParser.new
234
+ end
235
+
236
+ register "env_file_reader.env_file_tempfile_creator" do
237
+ KuberKit::EnvFileReader::EnvFileTempfileCreator.new
238
+ end
239
+
228
240
  register "env_file_reader.strategies.artifact_file" do
229
241
  KuberKit::EnvFileReader::Strategies::ArtifactFile.new
230
242
  end
231
243
 
244
+ register "env_file_reader.strategies.env_group" do
245
+ KuberKit::EnvFileReader::Strategies::EnvGroup.new
246
+ end
247
+
232
248
  register "template_reader.action_handler" do
233
249
  KuberKit::TemplateReader::ActionHandler.new
234
250
  end
@@ -257,6 +273,10 @@ class KuberKit::Container
257
273
  KuberKit::ServiceDeployer::ServiceListResolver.new
258
274
  end
259
275
 
276
+ register "service_deployer.service_dependency_resolver" do
277
+ KuberKit::ServiceDeployer::ServiceDependencyResolver.new
278
+ end
279
+
260
280
  register "service_deployer.strategies.kubernetes" do
261
281
  KuberKit::ServiceDeployer::Strategies::Kubernetes.new
262
282
  end
@@ -3,6 +3,7 @@ class KuberKit::Core::Artifacts::ArtifactStore
3
3
  store.add(artifact.name, artifact)
4
4
  end
5
5
 
6
+ Contract Symbol => Maybe[KuberKit::Core::Artifacts::AbstractArtifact]
6
7
  def get(artifact_name)
7
8
  artifact = get_from_configuration(artifact_name) ||
8
9
  get_global(artifact_name)
@@ -10,10 +11,12 @@ class KuberKit::Core::Artifacts::ArtifactStore
10
11
  artifact
11
12
  end
12
13
 
14
+ Contract Symbol => Maybe[KuberKit::Core::Artifacts::AbstractArtifact]
13
15
  def get_global(artifact_name)
14
16
  store.get(artifact_name)
15
17
  end
16
18
 
19
+ Contract Symbol => Maybe[KuberKit::Core::Artifacts::AbstractArtifact]
17
20
  def get_from_configuration(artifact_name)
18
21
  artifacts = KuberKit.current_configuration.artifacts
19
22
  artifacts[artifact_name]
@@ -1,6 +1,6 @@
1
1
  class KuberKit::Core::Configuration
2
2
  attr_reader :name, :artifacts, :registries, :env_files, :templates, :kubeconfig_path,
3
- :services_attributes, :enabled_services, :build_servers, :global_build_vars,
3
+ :services_attributes, :enabled_services, :disabled_services, :build_servers, :global_build_vars,
4
4
  :deployer_strategy, :deployer_namespace, :deployer_require_confirimation
5
5
 
6
6
  Contract KeywordArgs[
@@ -12,6 +12,7 @@ class KuberKit::Core::Configuration
12
12
  kubeconfig_path: Maybe[String],
13
13
  services_attributes: HashOf[Symbol => Hash],
14
14
  enabled_services: ArrayOf[Symbol],
15
+ disabled_services: ArrayOf[Symbol],
15
16
  build_servers: ArrayOf[KuberKit::Core::BuildServers::AbstractBuildServer],
16
17
  global_build_vars: HashOf[Symbol => Any],
17
18
  deployer_strategy: Symbol,
@@ -19,7 +20,7 @@ class KuberKit::Core::Configuration
19
20
  deployer_require_confirimation: Bool,
20
21
  ] => Any
21
22
  def initialize(name:, artifacts:, registries:, env_files:, templates:, kubeconfig_path:,
22
- services_attributes:, enabled_services:, build_servers:, global_build_vars:,
23
+ services_attributes:, enabled_services:, disabled_services:, build_servers:, global_build_vars:,
23
24
  deployer_strategy:, deployer_namespace:, deployer_require_confirimation:)
24
25
  @name = name
25
26
  @artifacts = artifacts
@@ -30,6 +31,7 @@ class KuberKit::Core::Configuration
30
31
  @build_servers = build_servers
31
32
  @services_attributes = services_attributes
32
33
  @enabled_services = enabled_services
34
+ @disabled_services = disabled_services
33
35
  @global_build_vars = global_build_vars
34
36
  @deployer_strategy = deployer_strategy
35
37
  @deployer_namespace = deployer_namespace
@@ -12,6 +12,7 @@ class KuberKit::Core::ConfigurationDefinition
12
12
  @templates = {}
13
13
  @build_servers = []
14
14
  @enabled_services = []
15
+ @disabled_services = []
15
16
  @services_attributes = {}
16
17
  end
17
18
 
@@ -24,6 +25,7 @@ class KuberKit::Core::ConfigurationDefinition
24
25
  templates: @templates,
25
26
  kubeconfig_path: @kubeconfig_path,
26
27
  enabled_services: @enabled_services,
28
+ disabled_services: @disabled_services,
27
29
  build_servers: @build_servers,
28
30
  services_attributes: @services_attributes,
29
31
  global_build_vars: @global_build_vars,
@@ -116,6 +118,11 @@ class KuberKit::Core::ConfigurationDefinition
116
118
  raise KuberKit::Error, "#enabled_services method accepts only Array or Hash"
117
119
  end
118
120
 
121
+ def disabled_services(services)
122
+ @disabled_services += services.map(&:to_sym)
123
+ return self
124
+ end
125
+
119
126
  def service_attributes(services)
120
127
  @services_attributes = @services_attributes.merge(services)
121
128
  self
@@ -29,6 +29,7 @@ class KuberKit::Core::ConfigurationFactory
29
29
  build_servers: build_servers,
30
30
  services_attributes: configuration_attrs.services_attributes,
31
31
  enabled_services: configuration_attrs.enabled_services,
32
+ disabled_services: configuration_attrs.disabled_services,
32
33
  global_build_vars: configuration_attrs.global_build_vars || {},
33
34
  deployer_strategy: configuration_attrs.deployer_strategy || configs.deployer_strategy,
34
35
  deployer_namespace: configuration_attrs.deployer_namespace,
@@ -0,0 +1,75 @@
1
+ class KuberKit::Core::Dependencies::AbstractDependencyResolver
2
+ CircularDependencyError = Class.new(KuberKit::Error)
3
+ DependencyNotFoundError = Class.new(KuberKit::NotFoundError)
4
+
5
+ # Iterate over list of dependencies for items (including the items themself).
6
+ # Iteration will send the list to the callback block function
7
+ Contract Or[Symbol, ArrayOf[Symbol]], Proc => Any
8
+ def each_with_deps(item_names, &block)
9
+ resolved_dependencies = []
10
+ # Get first list of dependencies ready to resolve
11
+ next_dependencies = get_next(item_names, limit: dependency_batch_size)
12
+
13
+ # Call the block for each list of dependencies ready to resolve, then calculate the next list
14
+ while (next_dependencies - resolved_dependencies).any?
15
+ block.call(next_dependencies)
16
+ resolved_dependencies += next_dependencies
17
+ next_dependencies = get_next(item_names, resolved: resolved_dependencies, limit: dependency_batch_size)
18
+ end
19
+
20
+ (item_names - resolved_dependencies).each_slice(dependency_batch_size) do |group|
21
+ block.call(group)
22
+ end
23
+ end
24
+
25
+ # Returns next list of dependencies ready to resolve.
26
+ # Item is not ready to resolve if it has personal dependency.
27
+ # E.g. if "A" depends on "B" and "C", "C" depends on "D", then only "B" and "D" will be returned.
28
+ Contract Or[Symbol, ArrayOf[Symbol]], KeywordArgs[
29
+ resolved: Optional[ArrayOf[Symbol]],
30
+ limit: Optional[Maybe[Num]]
31
+ ] => Any
32
+ def get_next(item_names, resolved: [], limit: nil)
33
+ deps = Array(item_names).map { |i| get_recursive_deps(i) }.flatten.uniq
34
+
35
+ # Find out which dependencies are ready to resolve,
36
+ # they should not have unresolved personal dependencies
37
+ ready_to_resolve = deps.select do |dep_name|
38
+ unresolved_deps = get_deps(dep_name) - resolved
39
+ unresolved_deps.empty?
40
+ end
41
+ unresolved_deps = ready_to_resolve - resolved
42
+ unresolved_deps = unresolved_deps.take(limit) if limit
43
+ unresolved_deps
44
+ end
45
+
46
+ # Get all dependencies for items (including the items themself), without any limitations
47
+ Contract Or[Symbol, ArrayOf[Symbol]] => Any
48
+ def get_all(item_names)
49
+ deps = Array(item_names).map { |i| get_recursive_deps(i) }.flatten
50
+ (deps + item_names).uniq
51
+ end
52
+
53
+ def get_recursive_deps(item_name, dependency_tree: [])
54
+ deps = get_deps(item_name)
55
+
56
+ if dependency_tree.include?(item_name)
57
+ raise CircularDependencyError, "Circular dependency found for #{item_name}. Dependency tree: #{dependency_tree.inspect}"
58
+ end
59
+
60
+ child_deps = []
61
+ deps.each do |i|
62
+ child_deps += get_recursive_deps(i, dependency_tree: dependency_tree + [item_name])
63
+ end
64
+
65
+ (deps + child_deps).uniq
66
+ end
67
+
68
+ def get_deps(item_name)
69
+ raise "This method should be overriden"
70
+ end
71
+
72
+ def dependency_batch_size
73
+ raise "This method should be overriden"
74
+ end
75
+ end
@@ -6,4 +6,8 @@ class KuberKit::Core::EnvFiles::AbstractEnvFile
6
6
  def initialize(env_file_name)
7
7
  @name = env_file_name
8
8
  end
9
+
10
+ def uniq_name
11
+ @name.to_s
12
+ end
9
13
  end
@@ -6,4 +6,8 @@ class KuberKit::Core::EnvFiles::ArtifactFile < KuberKit::Core::EnvFiles::Abstrac
6
6
  @artifact_name = artifact_name
7
7
  @file_path = file_path
8
8
  end
9
+
10
+ def uniq_name
11
+ [@artifact_name.to_s, @name.to_s].join("-")
12
+ end
9
13
  end
@@ -3,6 +3,7 @@ class KuberKit::Core::EnvFiles::EnvFileStore
3
3
  store.add(env_file.name, env_file)
4
4
  end
5
5
 
6
+ Contract Symbol => Maybe[KuberKit::Core::EnvFiles::AbstractEnvFile]
6
7
  def get(env_file_name)
7
8
  env_file = get_from_configuration(env_file_name) ||
8
9
  get_global(env_file_name)
@@ -10,10 +11,12 @@ class KuberKit::Core::EnvFiles::EnvFileStore
10
11
  env_file
11
12
  end
12
13
 
14
+ Contract Symbol => Maybe[KuberKit::Core::EnvFiles::AbstractEnvFile]
13
15
  def get_global(env_file_name)
14
16
  store.get(env_file_name)
15
17
  end
16
18
 
19
+ Contract Symbol => Maybe[KuberKit::Core::EnvFiles::AbstractEnvFile]
17
20
  def get_from_configuration(env_file_name)
18
21
  env_files = KuberKit.current_configuration.env_files
19
22
  env_files[env_file_name]
@@ -0,0 +1,12 @@
1
+ class KuberKit::Core::EnvFiles::EnvGroup < KuberKit::Core::EnvFiles::AbstractEnvFile
2
+ attr_reader :env_files
3
+
4
+ def initialize(env_group_name, env_files:)
5
+ super(env_group_name)
6
+ @env_files = env_files
7
+ end
8
+
9
+ def uniq_name
10
+ "env-group-#{@name.to_s}"
11
+ end
12
+ end
@@ -1,5 +1,6 @@
1
1
  class KuberKit::Core::Image
2
- attr_reader :name, :dependencies, :registry, :dockerfile_path, :build_vars, :build_context_dir, :tag, :before_build_callback, :after_build_callback
2
+ attr_reader :name, :dependencies, :registry, :dockerfile_path, :build_vars, :build_context_dir, :tag,
3
+ :before_build_callback, :after_build_callback
3
4
 
4
5
  Contract KeywordArgs[
5
6
  name: Symbol,
@@ -3,6 +3,7 @@ class KuberKit::Core::Registries::RegistryStore
3
3
  store.add(registry.name, registry)
4
4
  end
5
5
 
6
+ Contract Symbol => Maybe[KuberKit::Core::Registries::AbstractRegistry]
6
7
  def get(registry_name)
7
8
  registry = get_from_configuration(registry_name) ||
8
9
  get_global(registry_name)
@@ -10,10 +11,12 @@ class KuberKit::Core::Registries::RegistryStore
10
11
  registry
11
12
  end
12
13
 
14
+ Contract Symbol => Maybe[KuberKit::Core::Registries::AbstractRegistry]
13
15
  def get_global(registry_name)
14
16
  store.get(registry_name)
15
17
  end
16
18
 
19
+ Contract Symbol => Maybe[KuberKit::Core::Registries::AbstractRegistry]
17
20
  def get_from_configuration(registry_name)
18
21
  registries = KuberKit.current_configuration.registries
19
22
  registries[registry_name]
@@ -1,18 +1,20 @@
1
1
  class KuberKit::Core::Service
2
2
  AttributeNotSet = Class.new(Indocker::Error)
3
3
 
4
- attr_reader :name, :template_name, :tags, :images, :attributes, :deployer_strategy
4
+ attr_reader :name, :dependencies, :template_name, :tags, :images, :attributes, :deployer_strategy
5
5
 
6
6
  Contract KeywordArgs[
7
7
  name: Symbol,
8
+ dependencies: ArrayOf[Symbol],
8
9
  template_name: Maybe[Symbol],
9
10
  tags: ArrayOf[Symbol],
10
11
  images: ArrayOf[Symbol],
11
12
  attributes: HashOf[Symbol => Any],
12
13
  deployer_strategy: Maybe[Symbol]
13
14
  ] => Any
14
- def initialize(name:, template_name:, tags:, images:, attributes:, deployer_strategy:)
15
+ def initialize(name:, dependencies:, template_name:, tags:, images:, attributes:, deployer_strategy:)
15
16
  @name = name
17
+ @dependencies = dependencies
16
18
  @template_name = template_name
17
19
  @tags = tags
18
20
  @images = images
@@ -1,22 +1,29 @@
1
1
  class KuberKit::Core::ServiceDefinition
2
- attr_reader :service_name, :template_name
2
+ attr_reader :service_name, :template_name, :dependencies
3
3
 
4
4
  Contract Or[Symbol, String] => Any
5
5
  def initialize(service_name)
6
6
  @service_name = service_name.to_sym
7
+ @dependencies = []
7
8
  end
8
9
 
9
10
  def to_service_attrs
10
11
  OpenStruct.new(
11
- name: @service_name,
12
- template_name: get_value(@template_name),
13
- tags: Array(get_value(@tags)).map(&:to_sym),
14
- images: Array(get_value(@images)).map(&:to_sym),
15
- attributes: get_value(@attributes),
12
+ name: @service_name,
13
+ dependencies: @dependencies,
14
+ template_name: get_value(@template_name),
15
+ tags: Array(get_value(@tags)).map(&:to_sym),
16
+ images: Array(get_value(@images)).map(&:to_sym),
17
+ attributes: get_value(@attributes),
16
18
  deployer_strategy: get_value(@deployer_strategy),
17
19
  )
18
20
  end
19
21
 
22
+ def depends_on(*value, &block)
23
+ @dependencies = Array(value).flatten
24
+ self
25
+ end
26
+
20
27
  def template(value = nil, &block)
21
28
  @template_name = block_given? ? block : value
22
29
 
@@ -7,6 +7,7 @@ class KuberKit::Core::ServiceFactory
7
7
 
8
8
  KuberKit::Core::Service.new(
9
9
  name: service_attrs.name,
10
+ dependencies: service_attrs.dependencies,
10
11
  template_name: service_attrs.template_name,
11
12
  tags: service_attrs.tags,
12
13
  images: service_attrs.images,
@@ -3,6 +3,7 @@ class KuberKit::Core::Templates::TemplateStore
3
3
  store.add(template.name, template)
4
4
  end
5
5
 
6
+ Contract Symbol => Maybe[KuberKit::Core::Templates::AbstractTemplate]
6
7
  def get(template_name)
7
8
  template = get_from_configuration(template_name) ||
8
9
  get_global(template_name)
@@ -10,10 +11,12 @@ class KuberKit::Core::Templates::TemplateStore
10
11
  template
11
12
  end
12
13
 
14
+ Contract Symbol => Maybe[KuberKit::Core::Templates::AbstractTemplate]
13
15
  def get_global(template_name)
14
16
  store.get(template_name)
15
17
  end
16
18
 
19
+ Contract Symbol => Maybe[KuberKit::Core::Templates::AbstractTemplate]
17
20
  def get_from_configuration(template_name)
18
21
  templates = KuberKit.current_configuration.templates
19
22
  templates[template_name]
@@ -0,0 +1,51 @@
1
+ class KuberKit::EnvFileReader::EnvFileParser
2
+ # Parser is based on:
3
+ # https://github.com/bkeepers/dotenv/blob/master/lib/dotenv/parser.rb
4
+ LINE = /
5
+ (?:^|\A) # beginning of line
6
+ \s* # leading whitespace
7
+ (?:export\s+)? # optional export
8
+ ([\w\.]+) # key
9
+ (?:\s*=\s*?|:\s+?) # separator
10
+ ( # optional value begin
11
+ \s*'(?:\\'|[^'])*' # single quoted value
12
+ | # or
13
+ \s*"(?:\\"|[^"])*" # double quoted value
14
+ | # or
15
+ [^\#\r\n]+ # unquoted value
16
+ )? # value end
17
+ \s* # trailing whitespace
18
+ (?:\#.*)? # optional comment
19
+ (?:$|\z) # end of line
20
+ /x
21
+
22
+ Contract String => Hash
23
+ def call(string)
24
+ hash = {}
25
+ string.gsub(/\r\n?/, "\n").scan(LINE).each do |key, value|
26
+ hash[key] = parse_value(value || "")
27
+ end
28
+ hash
29
+ end
30
+
31
+ private
32
+
33
+ def parse_value(value)
34
+ # Remove surrounding quotes
35
+ value = value.strip.sub(/\A(['"])(.*)\1\z/m, '\2')
36
+
37
+ if Regexp.last_match(1) == '"'
38
+ value = unescape_characters(expand_newlines(value))
39
+ end
40
+
41
+ value
42
+ end
43
+
44
+ def unescape_characters(value)
45
+ value.gsub(/\\([^$])/, '\1')
46
+ end
47
+
48
+ def expand_newlines(value)
49
+ value.gsub('\n', "\n").gsub('\r', "\r")
50
+ end
51
+ end