amigrind 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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