amigrind 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +72 -0
  5. data/.travis.yml +4 -0
  6. data/CODE_OF_CONDUCT.md +49 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +201 -0
  9. data/README.md +112 -0
  10. data/Rakefile +6 -0
  11. data/amigrind.gemspec +36 -0
  12. data/bin/amigrind +8 -0
  13. data/lib/amigrind.rb +29 -0
  14. data/lib/amigrind/blueprints/aws_config.rb +56 -0
  15. data/lib/amigrind/blueprints/base_ami_source.rb +15 -0
  16. data/lib/amigrind/blueprints/blueprint.rb +22 -0
  17. data/lib/amigrind/blueprints/evaluator.rb +269 -0
  18. data/lib/amigrind/blueprints/parent_blueprint_source.rb +10 -0
  19. data/lib/amigrind/blueprints/provisioner.rb +20 -0
  20. data/lib/amigrind/blueprints/provisioners/file_upload.rb +29 -0
  21. data/lib/amigrind/blueprints/provisioners/local_shell.rb +28 -0
  22. data/lib/amigrind/blueprints/provisioners/remote_shell.rb +57 -0
  23. data/lib/amigrind/build/packer_runner.rb +106 -0
  24. data/lib/amigrind/build/rackerizer.rb +134 -0
  25. data/lib/amigrind/builder.rb +46 -0
  26. data/lib/amigrind/cli.rb +12 -0
  27. data/lib/amigrind/cli/_helpers.rb +49 -0
  28. data/lib/amigrind/cli/_root.rb +43 -0
  29. data/lib/amigrind/cli/blueprints/_category.rb +15 -0
  30. data/lib/amigrind/cli/blueprints/list.rb +21 -0
  31. data/lib/amigrind/cli/blueprints/show.rb +22 -0
  32. data/lib/amigrind/cli/build/_category.rb +15 -0
  33. data/lib/amigrind/cli/build/execute.rb +32 -0
  34. data/lib/amigrind/cli/build/print_packer.rb +28 -0
  35. data/lib/amigrind/cli/environments/_category.rb +15 -0
  36. data/lib/amigrind/cli/environments/list.rb +23 -0
  37. data/lib/amigrind/cli/environments/show.rb +22 -0
  38. data/lib/amigrind/cli/inventory/_category.rb +15 -0
  39. data/lib/amigrind/cli/inventory/add_to_channel.rb +28 -0
  40. data/lib/amigrind/cli/inventory/get-image.rb +34 -0
  41. data/lib/amigrind/cli/inventory/list.rb +14 -0
  42. data/lib/amigrind/cli/inventory/remove_from_channel.rb +28 -0
  43. data/lib/amigrind/cli/repo/_category.rb +15 -0
  44. data/lib/amigrind/cli/repo/init.rb +22 -0
  45. data/lib/amigrind/config.rb +89 -0
  46. data/lib/amigrind/environments/channel.rb +9 -0
  47. data/lib/amigrind/environments/environment.rb +66 -0
  48. data/lib/amigrind/environments/rb_evaluator.rb +7 -0
  49. data/lib/amigrind/repo.rb +217 -0
  50. data/lib/amigrind/version.rb +3 -0
  51. data/sample_config/config.yaml +9 -0
  52. data/sample_repo/.amigrind_root +0 -0
  53. data/sample_repo/blueprints/dependent_ubuntu.rb +21 -0
  54. data/sample_repo/blueprints/simple_ubuntu.rb +37 -0
  55. data/sample_repo/environments/development.yaml.example +15 -0
  56. metadata +288 -0
@@ -0,0 +1,22 @@
1
+ module Amigrind
2
+ module CLI
3
+ REPO.add_command(
4
+ Cri::Command.define do
5
+ name 'init'
6
+ description 'initializes new repo'
7
+
8
+ run do |_, args, _|
9
+ path = args.first
10
+
11
+ raise "A path is required to initialize a repo." if path.nil?
12
+
13
+ path = File.expand_path(path)
14
+
15
+ raise "Cannot initialize a repo into an existing path." if Dir.exist?(path)
16
+
17
+ Amigrind::Repo.init(path: path)
18
+ end
19
+ end
20
+ )
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ require 'settingslogic'
2
+ require 'os'
3
+
4
+ module Amigrind
5
+ # Config values we care about:
6
+ # `auto_profile` - boolean; if set
7
+ class Config < Settingslogic
8
+ extend Amigrind::Core::Logging::Mixin
9
+ include Amigrind::Core::Logging::Mixin
10
+
11
+ CREDENTIAL_TYPES = [ :default, :iam, :shared ].freeze
12
+
13
+ cfg_dir =
14
+ ENV['AMIGRIND_CONFIG_PATH'] || "#{Dir.home}/.amigrind"
15
+ cfg_file = "#{cfg_dir}/config.yaml"
16
+
17
+ unless Dir.exist?(cfg_dir)
18
+ info_log "initializing config directory"
19
+ FileUtils.mkdir_p(cfg_dir)
20
+ if OS.posix?
21
+ info_log "chmodding config directory"
22
+ FileUtils.chmod 'g=-rwx', cfg_dir
23
+ FileUtils.chmod 'o=-rwx', cfg_dir
24
+ end
25
+ end
26
+ unless File.exist?(cfg_file)
27
+ info_log "touching config file to create it"
28
+ FileUtils.touch(cfg_file)
29
+ if OS.posix?
30
+ info_log "chmodding config file"
31
+ FileUtils.chmod 'g=-rwx', cfg_file
32
+ FileUtils.chmod 'o=-rwx', cfg_file
33
+ end
34
+ end
35
+
36
+ source cfg_file
37
+
38
+ def aws_credentials(environment = nil)
39
+ # This is a minor disaster and should be refactored, but essentially boils down
40
+ # to specifying a credentials_type and any related settings. If
41
+ # `auto_profile_from_environment` is set, any time that an environment is
42
+ # passed in, `auto_profile_prefix` will be prepended to the environment name
43
+ # to generate the AWS profile name. This allows one to, for example, have
44
+ # a 'foocorp_production' profile that will be automatically used when working
45
+ # with the 'production' environment.
46
+ aws = Config['aws'] || {}
47
+
48
+ credential_type = (aws['credentials_type'] || :default).to_sym
49
+ auto_profile_from_environment = !!aws['auto_profile_from_environment']
50
+ auto_profile_prefix = aws['auto_profile_prefix'] || ''
51
+ profile_name = aws['profile_name']
52
+
53
+ debug_log "credentials_type: #{credential_type}"
54
+
55
+ raise "setting error: can only use profile_name with credential_type = shared." \
56
+ if credential_type != :shared && !profile_name.nil?
57
+
58
+ raise "setting error: cannot use both profile_name and auto_profile_from_environment." \
59
+ if !profile_name.nil? && auto_profile_from_environment
60
+
61
+ case credential_type
62
+ when :default
63
+ debug_log 'Using default credentials.'
64
+ nil
65
+ when :shared
66
+ if auto_profile_from_environment &&
67
+ profile_name.nil? && !environment.nil?
68
+ environment = environment.name \
69
+ if (environment.is_a?(Amigrind::Environments::Environment))
70
+
71
+ profile_name = "#{auto_profile_prefix}#{environment}"
72
+ debug_log "auto_profile_from_environment enabled and environment " \
73
+ "passed; setting profile_name to '#{profile_name}'."
74
+ end
75
+
76
+ p = (profile_name || 'default').strip
77
+
78
+ debug_log "Using profile '#{p}'."
79
+ Aws::SharedCredentials.new(profile_name: p)
80
+ when :iam
81
+ debug_log 'Using IAM credentials.'
82
+ Aws::InstanceProfileCredentials.new
83
+ else
84
+ raise "invalid credential type '#{credential_type}' " \
85
+ "(allowed: #{CREDENTIAL_TYPES.join(', ')})"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ module Amigrind
2
+ module Environments
3
+ class Channel
4
+ include Virtus.model
5
+
6
+ attribute :name, String
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,66 @@
1
+ module Amigrind
2
+ module Environments
3
+ class Environment
4
+ extend Amigrind::Core::Logging::Mixin
5
+
6
+ include Virtus.model(constructor: false, mass_assignment: false)
7
+
8
+ class AWSConfig
9
+ include Virtus.model
10
+
11
+ attribute :region, String
12
+ attribute :copy_regions, Array[String]
13
+ attribute :vpc, String
14
+ attribute :subnets, Array[String]
15
+ attribute :ssh_keypair_name, String
16
+ end
17
+
18
+ attribute :name, String
19
+ attribute :channels, Hash[String => Channel]
20
+ attribute :aws, AWSConfig
21
+ attr_reader :properties
22
+
23
+ def initialize
24
+ @aws = AWSConfig.new
25
+ @channels = []
26
+ @properties = {}
27
+ end
28
+
29
+ def self.from_yaml(name, yaml_input)
30
+ yaml = YAML.load(yaml_input).deep_symbolize_keys
31
+
32
+ yaml[:amigrind] ||= {}
33
+ yaml[:aws] ||= {}
34
+ yaml[:properties] ||= {}
35
+
36
+ env = Environment.new
37
+ env.name = name.to_s.strip.downcase
38
+
39
+ env.aws = AWSConfig.new(yaml[:aws])
40
+
41
+ env.properties.merge!(yaml[:properties])
42
+
43
+ env.channels = (yaml[:amigrind][:channels] || []).map do |k, v|
44
+ [ k.to_s, Channel.new(v.merge(name: k)) ]
45
+ end.to_h
46
+
47
+ # TODO: use these for validations
48
+ valid_mappings = {
49
+ 'root' => env,
50
+ 'aws' => env.aws
51
+ }
52
+
53
+ env
54
+ end
55
+
56
+ def self.load_yaml_file(path)
57
+ raise "'path' must be a String." unless path.is_a?(String)
58
+ raise "'path' must be a file that exists." unless File.exist?(path)
59
+ raise "'path' must end in .yml, .yaml, .yml.erb, or .yaml.erb." \
60
+ unless path.end_with?('.yml', '.yaml', '.yml.erb', '.yaml.erb')
61
+
62
+ Environment.from_yaml(File.basename(path, '.*'), Erubis::Eruby.new(File.read(path)).result)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ module Amigrind
2
+ module Environments
3
+ # TODO: implement Ruby environments
4
+ class RbEvaluator
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,217 @@
1
+ module Amigrind
2
+ class Repo
3
+ include Amigrind::Core::Logging::Mixin
4
+
5
+ attr_reader :path
6
+
7
+ def initialize(path)
8
+ @path = File.expand_path path
9
+
10
+ raise "'path' (#{path}) is not a directory." unless Dir.exist?(path)
11
+ raise "'path' is not an Amigrind root (lacks .amigrind_root file)." \
12
+ unless File.exist?(File.join(path, '.amigrind_root'))
13
+
14
+ info_log "using Amigrind path: #{path}"
15
+ end
16
+
17
+ def environments_path
18
+ File.join(path, 'environments')
19
+ end
20
+
21
+ def blueprints_path
22
+ File.join(path, 'blueprints')
23
+ end
24
+
25
+ # TODO: Ruby DSL environments
26
+ def environment_names
27
+ yaml_environments =
28
+ Dir[File.join(environments_path, '*.yaml')] \
29
+ .map { |f| File.basename(f, '.yaml').to_s.strip.downcase }
30
+
31
+ rb_environments =
32
+ [].map { |f| File.basename(f, '.rb').to_s.strip.downcase }
33
+
34
+ duplicate_environments = yaml_environments & rb_environments
35
+ duplicate_environments.each do |dup_env_name|
36
+ warn_log "environment '#{dup_env_name}' found in both YAML and Ruby; skipping."
37
+ end
38
+
39
+ (yaml_environments + rb_environments - duplicate_environments).sort
40
+ end
41
+
42
+ # TODO: cache environments (but make configurable)
43
+ def environment(name)
44
+ yaml_path = yaml_path_if_exists(name)
45
+ rb_path = rb_path_if_exists(name)
46
+
47
+ raise "found multiple env files for same env #{name}." if !yaml_path.nil? && !rb_path.nil?
48
+ raise "TODO: implement Ruby environments." unless rb_path.nil?
49
+
50
+ env = Environments::Environment.load_yaml_file(yaml_path) unless yaml_path.nil?
51
+
52
+ raise "no env found for '#{name}'." if env.nil?
53
+
54
+ IceNine.deep_freeze(env)
55
+ env
56
+ end
57
+
58
+ def with_environment(environment_name, &block)
59
+ block.call(environment(environment_name))
60
+ end
61
+
62
+ def blueprint_names
63
+ Dir[File.join(blueprints_path, "*.rb")].map { |f| File.basename(f, ".rb") }
64
+ end
65
+
66
+ # TODO: cache blueprint/environment tuples (but make configurable)
67
+ def evaluate_blueprint(blueprint_name, env)
68
+ raise "'env' must be a String or an Environment." \
69
+ unless env.is_a?(String) || env.is_a?(Environments::Environment)
70
+
71
+ if env.is_a?(String)
72
+ env = environment(env)
73
+ end
74
+
75
+ ev = Amigrind::Blueprints::Evaluator.new(File.join(blueprints_path,
76
+ "#{blueprint_name}.rb"),
77
+ env)
78
+
79
+ ev.blueprint
80
+ end
81
+
82
+ # TODO: refactor these client-y things.
83
+ def add_to_channel(env, blueprint_name, id, channel)
84
+ raise "'env' must be a String or an Environment." \
85
+ unless env.is_a?(String) || env.is_a?(Environments::Environment)
86
+ raise "'blueprint_name' must be a String." unless blueprint_name.is_a?(String)
87
+ raise "'id' must be a Fixnum." unless id.is_a?(Fixnum)
88
+ raise "'channel' must be a String or Symbol." \
89
+ unless channel.is_a?(String) || channel.is_a?(Symbol)
90
+
91
+ if env.is_a?(String)
92
+ env = environment(env)
93
+ end
94
+
95
+ raise "channel '#{channel}' does not exist in environment '#{env.name}'." \
96
+ unless env.channels.key?(channel.to_s) || channel.to_sym == :latest
97
+
98
+ credentials = Amigrind::Config.aws_credentials(env)
99
+
100
+ amigrind_client = Amigrind::Core::Client.new(env.aws.region, credentials)
101
+ ec2 = Aws::EC2::Client.new(region: env.aws.region, credentials: credentials)
102
+
103
+ image = amigrind_client.get_image_by_id(name: blueprint_name, id: id)
104
+
105
+ tag_key = Amigrind::Core::AMIGRIND_CHANNEL_TAG % { channel_name: channel }
106
+
107
+ info_log "setting '#{tag_key}' on image #{image.id}..."
108
+ ec2.create_tags(
109
+ resources: [ image.id ],
110
+ tags: [
111
+ {
112
+ key: tag_key,
113
+ value: '1'
114
+ }
115
+ ]
116
+ )
117
+ end
118
+
119
+ def remove_from_channel(env, blueprint_name, id, channel)
120
+ raise "'env' must be a String or an Environment." \
121
+ unless env.is_a?(String) || env.is_a?(Environments::Environment)
122
+ raise "'blueprint_name' must be a String." unless blueprint_name.is_a?(String)
123
+ raise "'id' must be a Fixnum." unless id.is_a?(Fixnum)
124
+ raise "'channel' must be a String or Symbol." \
125
+ unless channel.is_a?(String) || channel.is_a?(Symbol)
126
+
127
+ if env.is_a?(String)
128
+ env = environment(env)
129
+ end
130
+
131
+ raise "channel '#{channel}' does not exist in environment '#{env.name}'." \
132
+ unless env.channels.key?(channel.to_s) || channel.to_sym == :latest
133
+
134
+ credentials = Amigrind::Config.aws_credentials(env)
135
+
136
+ amigrind_client = Amigrind::Core::Client.new(env.aws.region, credentials)
137
+ ec2 = Aws::EC2::Client.new(region: env.aws.region, credentials: credentials)
138
+
139
+ image = amigrind_client.get_image_by_id(name: blueprint_name, id: id)
140
+
141
+ tag_key = Amigrind::Core::AMIGRIND_CHANNEL_TAG % { channel_name: channel }
142
+
143
+ info_log "clearing '#{tag_key}' on image #{image.id}..."
144
+ ec2.delete_tags(
145
+ resources: [ image.id ],
146
+ tags: [
147
+ {
148
+ key: tag_key,
149
+ value: nil
150
+ }
151
+ ]
152
+ )
153
+ end
154
+
155
+ def get_image_by_channel(env, blueprint_name, channel, steps_back = 0)
156
+ raise "'env' must be a String or an Environment." \
157
+ unless env.is_a?(String) || env.is_a?(Environments::Environment)
158
+ raise "'blueprint_name' must be a String." unless blueprint_name.is_a?(String)
159
+ raise "'channel' must be a String or Symbol." \
160
+ unless channel.is_a?(String) || channel.is_a?(Symbol)
161
+
162
+ if env.is_a?(String)
163
+ env = environment(env)
164
+ end
165
+
166
+ raise "channel '#{channel}' does not exist in environment '#{env.name}'." \
167
+ unless env.channels.key?(channel.to_s) || channel.to_sym == :latest
168
+
169
+ credentials = Amigrind::Config.aws_credentials(env)
170
+ amigrind_client = Amigrind::Core::Client.new(env.aws.region, credentials)
171
+
172
+ amigrind_client.get_image_by_channel(name: blueprint_name, channel: channel, steps_back: steps_back)
173
+ end
174
+
175
+ class << self
176
+ def init(path:)
177
+ raise "TODO: implement"
178
+ end
179
+
180
+ def with_repo(path: nil, &block)
181
+ path = path || ENV['AMIGRIND_PATH'] || Dir.pwd
182
+
183
+ repo = Repo.new(path)
184
+
185
+ Dir.chdir path do
186
+ block.call(repo)
187
+ end
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ def yaml_path_if_exists(name)
194
+ matches = [
195
+ "#{environments_path}/#{name}.yml",
196
+ "#{environments_path}/#{name}.yaml",
197
+ "#{environments_path}/#{name}.yml.erb",
198
+ "#{environments_path}/#{name}.yaml.erb"
199
+ ].select { |f| File.exist?(f) }
200
+
201
+ case matches.size
202
+ when 0
203
+ nil
204
+ when 1
205
+ matches.first
206
+ else
207
+ raise "found multiple env files for same env #{name}."
208
+ end
209
+ end
210
+
211
+ def rb_path_if_exists(name)
212
+ path = "#{environments_path}/#{name}.rb"
213
+
214
+ File.exist?(path) ? path : nil
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,3 @@
1
+ module Amigrind
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ aws:
2
+ # IF YOU ARE USING SHARED CREDENTIALS (~/.aws/credentials):
3
+ # ---------------------------------------------------------
4
+ credentials_type: shared
5
+ profile_name: default
6
+
7
+ amigrind:
8
+ # Without this, you'll need to pass the --environment argument all over the place.
9
+ default_environment: development
File without changes
@@ -0,0 +1,21 @@
1
+ source :parent do
2
+ name 'simple_ubuntu'
3
+ channel :live
4
+ end
5
+
6
+ build_channel :prerelease
7
+
8
+ aws do
9
+ instance_type 't2.micro'
10
+ ssh_username 'ubuntu'
11
+
12
+ associate_public_ip_address true
13
+ end
14
+
15
+ provisioner :something_else, RemoteShell do
16
+ run_as_root!
17
+
18
+ command <<-CMD
19
+ echo "dependent_ubuntu" > /MACHINE_TYPE
20
+ CMD
21
+ end