dockerize 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Njg4NjU1MjBhNDkyMmFlMzMwYjhiODIzNDM3MzEwY2RiMzM2NDg2Mg==
5
+ data.tar.gz: !binary |-
6
+ Nzg0ZjUwNmE4YWUwZWY0MmFjYjczZDlkZmI2NTQwYzZkNGUwOTczMQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZTg2OTdjMGJiZDJmYWRlYWJmNTMyYTgzNjczNjU0YjcyMzJmZTNlNzY0YmY2
10
+ MTVkMjVlZDRjMGQwN2EzNGU4YTNhMDIxZWNiZmVhZDdmMjY0YWY2ZTJiZTZl
11
+ YmVlMTViOWU0NThlNmMxOTkxMjczYjYyZjFhZGEzYTE0YzYyZDE=
12
+ data.tar.gz: !binary |-
13
+ ZTVmMTQ3ZmFhNGM5YWFjZjE2MzNmMGNkZWJmM2IyNjlkNzJkMGUzOTJhYjg1
14
+ OTE2YTdkYjY4MTU5YjExYzhiMGNiYzc1YzdlNjA0ZGNhODcwYWVmMzU3N2I5
15
+ MmFmNjRkZWU0Y2M2ODgxYTQxYTU1NDIxNWIyNjc2YTVhN2I5Mjc=
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+ Gemfile.lock
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
20
+ .ruby-version
data/.jrubyrc ADDED
@@ -0,0 +1,5 @@
1
+ compat.version=1.9
2
+ ctext.enabled=false
3
+ errno.backtrace=true
4
+
5
+ # vim:filetype=jproperties
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --require English
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ AllCops:
3
+ Excludes:
4
+ - vendor/**
5
+
6
+ Documentation:
7
+ Enabled: false
8
+
9
+ GlobalVars:
10
+ Enabled: false
data/.simplecov ADDED
@@ -0,0 +1,6 @@
1
+ # vim:fileencoding=utf-8
2
+ if ENV['COVERAGE']
3
+ SimpleCov.start do
4
+ add_filter '/spec/'
5
+ end
6
+ end
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ language: ruby
3
+ matrix:
4
+ allow_failures:
5
+ - rvm: jruby-19mode
6
+ rvm:
7
+ - 1.9.3
8
+ - 2.0.0
9
+ - jruby-19mode
10
+ notifications:
11
+ email:
12
+ recipients:
13
+ - r.colton@modcloth.com
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dockerize.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Rafe Colton
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Dockerize
2
+
3
+ [![Build Status](https://travis-ci.org/modcloth-labs/dockerize.png?branch=master)](https://travis-ci.org/modcloth-labs/dockerize)
4
+
5
+ Dockerizes your project.
6
+
7
+ ## About
8
+
9
+ Dockerize helps 'dockerize' your application by providing you an easy
10
+ way to add Docker integration to your project. Once a project has been
11
+ 'dockerized,' you will have some useful files in place that will guide
12
+ you through the process of deploying your application with Docker.
13
+
14
+ In particular, `dockerize` creates the `.run` directory in your project.
15
+ Use this directory to configure important files that need to be placed
16
+ on the host machine *outside* your container at deploy time. These
17
+ files might include an [Upstart](http://upstart.ubuntu.com/cookbook/)
18
+ config file, for example. Follow the comments in the files for more
19
+ details
20
+
21
+ ## Installation
22
+
23
+ Install with:
24
+
25
+ ```bash
26
+ > gem install dockerize
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ The simplest use case is dockerizing the current project. Example
32
+
33
+ ```bash
34
+ > dockerize .
35
+ ```
36
+
37
+ Dockerize is also very configurable, and allows many options to be set
38
+ through the command line.
39
+
40
+ To see what options are available, run `dockerize` with the help flag:
41
+
42
+ ```bash
43
+ > dockerize --help
44
+
45
+ # Usage: dockerize <project directory> [options]
46
+ # Options:
47
+ # --quiet, -q: Silence output
48
+ # --dry-run, -d: Dry run, do not write any files
49
+ # --force, -f: Force existing files to be overwritten
50
+ # --backup, --no-backup, -b: Creates .bak version of files before overwriting them
51
+ # --registry, -r <s>: The Docker registry to use when writing files
52
+ # --template-dir, -t <s>: The directory containing the templates to be written
53
+ # --maintainer, -m <s>: The default MAINTAINER to use for any Dockerfiles written
54
+ # --from, -F <s>: The default base image to use for any Dockerfiles written
55
+ # --version, -v: Print version and exit
56
+ # --help, -h: Show this message
57
+ ```
58
+
59
+ If you want to use `dockerize` to dockerize multiple projects, it may be
60
+ useful to set some defaults in the environment. Options currently
61
+ configurable in the environment include:
62
+
63
+ * default registry - `DOCKERIZE_REGISTRY`
64
+ * template dir - `DOCKERIZE_TEMPLATE_DIR`
65
+ * default maintainer - `DOCKERIZE_MAINTAINER`
66
+ * default base image - `DOCKERIZE_FROM`
67
+
68
+ ## Deploying
69
+
70
+ This gem also provides a handy script for unpacking the `.run` directory
71
+ at deploy time. To run this script, use the following command:
72
+
73
+ ```bash
74
+ # retrieve the script
75
+ > curl -s -O https://raw.github.com/modcloth-labs/dockerize/master/bin/dockerize-unpack
76
+
77
+ # make it executable
78
+ > chmod +x dockerize-unpack
79
+
80
+ # run the script
81
+ > dockerize-unpack quay.io/yourorg/example-image:tag
82
+
83
+ # or, run with the help flag for more info
84
+ > dockerize-unpack --help
85
+ ```
86
+
87
+
88
+ ## Contributing
89
+
90
+ 1. Fork it
91
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
92
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
93
+ 4. Push to the branch (`git push origin my-new-feature`)
94
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ # vim:fileencoding=utf-8
3
+
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+
7
+ desc 'Run rubocop'
8
+ task :rubocop do
9
+ sh('rubocop --format simple'){ |ok, _| ok || abort }
10
+ end
11
+
12
+ RSpec::Core::RakeTask.new(:spec) do |t|
13
+ t.rspec_opts = '--format documentation'
14
+ end
15
+
16
+ task default: [:spec, :rubocop]
data/bin/dockerize ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ lib = File.expand_path('../../lib', __FILE__)
5
+ vendor = File.expand_path('../../vendor', __FILE__)
6
+
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+ $LOAD_PATH.unshift(vendor) unless $LOAD_PATH.include?(vendor)
9
+
10
+ require 'dockerize'
11
+ require 'dockerize/cli'
12
+ require 'colored'
13
+
14
+ Dockerize::Cli.run(ARGV)
15
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -x
4
+ set -e
5
+
6
+ function usage() {
7
+ cat <<EOB
8
+
9
+ Usage: dockerize-unpack <image> [docker args]
10
+
11
+ Options:
12
+ -h/--help Display this message
13
+
14
+ Unpacks the files shipped with a docker container (via the \`dockerize\` gem)
15
+ and puts them in place by calling the \`deploy\` target in the
16
+ \`Makefile.run\`
17
+ EOB
18
+ }
19
+
20
+ function main() {
21
+ case "$1" in
22
+ -h|--help)
23
+ usage
24
+ exit 0
25
+ ;;
26
+ esac
27
+
28
+ export DOCKER="${DOCKER:-sudo docker}"
29
+ export IMAGE="$1"
30
+ if [[ -z "$IMAGE" ]] ; then
31
+ usage
32
+ exit 1
33
+ fi
34
+ export ARGS="$2"
35
+
36
+ mkdir -p /tmp/docker-bridge
37
+ pushd /tmp/docker-bridge
38
+ fetch
39
+ unpack
40
+ popd
41
+ }
42
+
43
+ function fetch() {
44
+ $DOCKER run -v /tmp/docker-bridge:/bridge "$ARGS" "$IMAGE" /docker/run/bridge
45
+ tar -xzf /tmp/docker-bridge/run.tar.gz
46
+ }
47
+
48
+ function unpack() {
49
+ cd ./docker/run
50
+ make -f Makefile.run deploy
51
+ }
52
+
53
+ main "$@"
data/dockerize.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ vendor = File.expand_path('../vendor', __FILE__)
4
+
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ $LOAD_PATH.unshift(vendor) unless $LOAD_PATH.include?(vendor)
7
+
8
+ require 'dockerize/version'
9
+
10
+ Gem::Specification.new do |gem|
11
+ gem.name = 'dockerize'
12
+ gem.version = Dockerize::VERSION
13
+ gem.authors = ['Rafe Colton']
14
+ gem.email = ['r.colton@modcloth.com']
15
+ gem.description = 'Dockerizes your application'
16
+ gem.summary = 'Creates a templated Dockerfile and corresponding ' <<
17
+ 'support files for easy deployment with docker.'
18
+ gem.homepage = 'https://github.com/modcloth-labs/dockerize'
19
+ gem.license = 'MIT'
20
+
21
+ gem.files = `git ls-files`.split($/)
22
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ gem.bindir = 'bin'
24
+ gem.test_files = gem.files.grep(%r{^spec/})
25
+ gem.require_paths = %w(lib vendor)
26
+ gem.required_ruby_version = '>= 1.9.3'
27
+
28
+ gem.add_development_dependency 'rake'
29
+ gem.add_development_dependency 'rspec'
30
+ gem.add_development_dependency 'rubocop'
31
+
32
+ gem.add_development_dependency 'pry' unless RUBY_PLATFORM == 'java'
33
+ gem.add_development_dependency 'simplecov' unless RUBY_PLATFORM == 'java'
34
+
35
+ gem.add_runtime_dependency 'syck' if RUBY_VERSION.split('.').first.to_i >= 2
36
+ end
@@ -0,0 +1,52 @@
1
+ # coding: utf-8
2
+
3
+ require 'dockerize/template_parser'
4
+ require 'dockerize/document_writer'
5
+
6
+ module Dockerize
7
+ class Cli
8
+ class << self
9
+ attr_reader :args
10
+
11
+ def run(argv = [])
12
+ @args = argv
13
+ ensure_project_dir
14
+ parse_args
15
+ set_out_stream
16
+ handle_templates
17
+ end
18
+
19
+ private
20
+
21
+ attr_writer :args
22
+
23
+ def handle_templates
24
+ all_templates.map do |template|
25
+ Dockerize::TemplateParser.new(File.read(template))
26
+ .write_with Dockerize::DocumentWriter.new
27
+ end
28
+ end
29
+
30
+ def all_templates
31
+ Dir["#{Dockerize::Config.template_dir}/**/*.erb"] |
32
+ Dir["#{Dockerize::Config.template_dir}/**/*.erb"]
33
+ end
34
+
35
+ def set_out_stream
36
+ $out = $stdout
37
+ $out = File.open('/dev/null', 'w') if Dockerize::Config.quiet?
38
+ end
39
+
40
+ def parse_args
41
+ Dockerize::Config.parse(args)
42
+ end
43
+
44
+ def ensure_project_dir
45
+ if args.count < 1 && !%w(-h --help).include?(args[0])
46
+ fail Dockerize::Error::MissingRequiredArgument,
47
+ 'You must specify a project directory to dockerize'
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,141 @@
1
+ # coding: utf-8
2
+ # rubocop:disable MethodLength, ClassLength, CyclomaticComplexity
3
+
4
+ require 'trollop'
5
+
6
+ module Dockerize
7
+ class Config
8
+ class << self
9
+ attr_reader :project_dir
10
+ attr_accessor :opts
11
+
12
+ def parse(args)
13
+ config = self
14
+
15
+ Trollop.options(args) do
16
+ text "Usage: dockerize <project directory> [options]\nOptions:\n"
17
+
18
+ # -q/--quiet
19
+ opt :quiet, 'Silence output', type: :flag, short: 'q', default: false
20
+
21
+ # -d/--dry-run
22
+ opt :dry_run, 'Dry run, do not write any files',
23
+ type: :flag,
24
+ short: 'd',
25
+ default: false
26
+
27
+ # -f/--force
28
+ opt :force, 'Force existing files to be overwritten',
29
+ type: :flag,
30
+ short: 'f',
31
+ default: false
32
+
33
+ # -b/--backup
34
+ opt :backup, 'Creates .bak version of files before overwriting them',
35
+ type: :flag,
36
+ short: 'b',
37
+ default: true
38
+
39
+ # -r/--registry
40
+ opt :registry, 'The Docker registry to use when writing files',
41
+ type: :string,
42
+ short: 'r',
43
+ default: ENV['DOCKERIZE_REGISTRY'] || 'quay.io/modcloth'
44
+
45
+ # -t/--template-dir
46
+ opt :template_dir,
47
+ 'The directory containing the templates to be written',
48
+ type: :string,
49
+ short: 't',
50
+ default: ENV['DOCKERIZE_TEMPLATE_DIR'] ||
51
+ "#{config.top}/templates"
52
+
53
+ # -m/--maintainer
54
+ opt :maintainer,
55
+ 'The default MAINTAINER to use for any Dockerfiles written',
56
+ type: :string,
57
+ short: 'm',
58
+ default: ENV['DOCKERIZE_MAINTAINER'] ||
59
+ "#{ENV['USER']} <#{ENV['USER']}@example.com>"
60
+
61
+ # -F/--from
62
+ opt :from,
63
+ 'The default base image to use for any Dockerfiles written',
64
+ type: :string,
65
+ short: 'F',
66
+ default: ENV['DOCKERIZE_FROM'] || 'ubuntu:12.04'
67
+
68
+ version "dockerize #{Dockerize::VERSION}"
69
+
70
+ begin
71
+ config.send(:opts=, parse(args))
72
+ rescue Trollop::CommandlineError => e
73
+ $stderr.puts "Error: #{e.message}."
74
+ $stderr.puts 'Try --help for help.'
75
+ exit 1
76
+ rescue Trollop::HelpNeeded
77
+ educate
78
+ exit
79
+ rescue Trollop::VersionNeeded
80
+ $stderr.puts version
81
+ exit
82
+ end
83
+
84
+ config.send(:opts)[:top] = config.top
85
+ config.send(:generate_accessor_methods, self)
86
+ end
87
+
88
+ self.project_dir = args[0]
89
+ set_project_name unless opts[:project_name]
90
+ end
91
+
92
+ def project_dir=(dir)
93
+ unless dir
94
+ fail Dockerize::Error::UnspecifiedProjectDirectory,
95
+ 'You must specify a project directory'
96
+ end
97
+
98
+ expanded_dir = File.expand_path(dir)
99
+
100
+ if !File.exists?(expanded_dir)
101
+ fail Dockerize::Error::NonexistentProjectDirectory,
102
+ "Project directory '#{expanded_dir}' does not exist"
103
+ elsif !File.directory?(expanded_dir)
104
+ fail Dockerize::Error::InvalidProjectDirectory,
105
+ "Project directory '#{expanded_dir}' is not a directory"
106
+ else
107
+ @project_dir = expanded_dir
108
+ end
109
+ end
110
+
111
+ def set_project_name
112
+ opts[:project_name] ||= File.basename(project_dir)
113
+ end
114
+
115
+ def top
116
+ @top ||= Gem::Specification.find_by_name('dockerize').gem_dir
117
+ end
118
+
119
+ private
120
+
121
+ def klass
122
+ @klass ||= class << self ; self ; end
123
+ end
124
+
125
+ def add_method(name, &block)
126
+ klass.send(:define_method, name.to_sym, &block)
127
+ end
128
+
129
+ def generate_accessor_methods(parser)
130
+ parser.specs.map do |k, v|
131
+ case v[:type]
132
+ when *Trollop::Parser::FLAG_TYPES
133
+ add_method("#{k}?") { @opts[k] }
134
+ when :string
135
+ add_method("#{k}") { @opts[k] }
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,91 @@
1
+ # coding: utf-8
2
+
3
+ require 'fileutils'
4
+ require 'colored'
5
+
6
+ module Dockerize
7
+ class DocumentWriter
8
+ CREATE_WORD = 'created '.green
9
+ REPLACE_WORD = 'replaced '.red
10
+ IGNORE_WORD = 'ignored '.yellow
11
+
12
+ attr_writer :document_name
13
+
14
+ def initialize(document_name = nil, stream = $out)
15
+ @stream = stream
16
+ @document_name = document_name
17
+ end
18
+
19
+ def write(contents = nil, executable = false)
20
+ @invalid_content = true unless contents
21
+ ensure_containing_dir
22
+ do_backup! if should_backup?
23
+ inform_of_write(status_word)
24
+ do_write!(contents, executable) if should_write?
25
+ end
26
+
27
+ def output_target
28
+ "#{Dockerize::Config.project_dir}/#{document_name}"
29
+ end
30
+
31
+ def inform_of_write(type)
32
+ $out.puts ' ' << type << document_name
33
+ end
34
+
35
+ protected
36
+
37
+ def invalid_content?
38
+ @invalid_content.nil? ? false : @invalid_content
39
+ end
40
+
41
+ def invalid_word
42
+ 'The template provided contains invalid content: '.blue
43
+ end
44
+
45
+ def status_word
46
+ if invalid_content?
47
+ invalid_word
48
+ elsif !should_write?
49
+ IGNORE_WORD
50
+ elsif preexisting_file?
51
+ REPLACE_WORD
52
+ else
53
+ CREATE_WORD
54
+ end
55
+ end
56
+
57
+ def should_backup?
58
+ Dockerize::Config.backup? && preexisting_file?
59
+ end
60
+
61
+ def should_write?
62
+ (Dockerize::Config.force? || !preexisting_file?) && !invalid_content?
63
+ end
64
+
65
+ def preexisting_file?
66
+ File.exists?(output_target)
67
+ end
68
+
69
+ def ensure_containing_dir(target = output_target)
70
+ FileUtils.mkdir_p(File.dirname(target))
71
+ end
72
+
73
+ def do_write!(contents, executable)
74
+ @stream = File.open(output_target, 'w') unless Dockerize::Config.dry_run?
75
+ @stream.print contents
76
+ FileUtils.chmod('+x', output_target) if executable && @stream != $out
77
+ ensure
78
+ @stream.close unless @stream == $out
79
+ end
80
+
81
+ def do_backup!
82
+ FileUtils.cp(output_target, "#{output_target}.bak")
83
+ end
84
+
85
+ def document_name
86
+ return @document_name if @document_name
87
+ fail Dockerize::Error::DocumentNameNotSpecified,
88
+ "Document name not specified for class #{self.class.name}"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # coding: utf-8
2
+
3
+ module Dockerize
4
+ module Error
5
+ # Invalid Config Errors
6
+ InvalidConfig = Class.new(StandardError)
7
+ NonexistentProjectDirectory = Class.new(InvalidConfig)
8
+ InvalidProjectDirectory = Class.new(InvalidConfig)
9
+ UnspecifiedProjectDirectory = Class.new(InvalidConfig)
10
+
11
+ # General Errors
12
+ MissingRequiredArgument = Class.new(ArgumentError)
13
+ DocumentNameNotSpecified = Class.new(NoMethodError)
14
+ end
15
+ end
@@ -0,0 +1,63 @@
1
+ # coding: utf-8
2
+
3
+ require 'ostruct'
4
+ require 'erb'
5
+
6
+ module Dockerize
7
+ class TemplateParser
8
+ attr_reader :raw_text
9
+ attr_reader :metadata
10
+
11
+ def initialize(contents)
12
+ @raw_text = contents
13
+ @metadata = []
14
+ end
15
+
16
+ def document_name
17
+ metadata[:filename]
18
+ end
19
+
20
+ def executable?
21
+ metadata[:executable] == true ? true : false
22
+ end
23
+
24
+ def write_with(writer)
25
+ text = parsed_erb
26
+ writer.document_name = document_name
27
+ writer.write(text, executable?)
28
+ end
29
+
30
+ def parsed_erb
31
+ return @parsed_erb if @parsed_erb
32
+ begin
33
+ @parsed_erb = parse_erb(raw_text, config_vars)
34
+ rescue SyntaxError
35
+ @parsed_erb = nil
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def config_vars
42
+ Dockerize::Config.opts
43
+ end
44
+
45
+ def parse_erb(raw, hash)
46
+ os = OpenStruct.new(hash)
47
+ os_before = os.clone
48
+ result = ERB.new(raw, nil, '%<>>-').result(
49
+ os.instance_eval { binding }
50
+ )
51
+ @metadata = hash_diff(os, os_before)
52
+ result
53
+ end
54
+
55
+ def hash_diff(os1, os2)
56
+ h1 = os1.marshal_dump
57
+ h2 = os2.marshal_dump
58
+ h1.dup.delete_if { |k, v| h2[k] == v }.merge!(
59
+ h2.dup.delete_if { |k, v| h1.key?(k) }
60
+ )
61
+ end
62
+ end
63
+ end