amigrind 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +72 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +201 -0
- data/README.md +112 -0
- data/Rakefile +6 -0
- data/amigrind.gemspec +36 -0
- data/bin/amigrind +8 -0
- data/lib/amigrind.rb +29 -0
- data/lib/amigrind/blueprints/aws_config.rb +56 -0
- data/lib/amigrind/blueprints/base_ami_source.rb +15 -0
- data/lib/amigrind/blueprints/blueprint.rb +22 -0
- data/lib/amigrind/blueprints/evaluator.rb +269 -0
- data/lib/amigrind/blueprints/parent_blueprint_source.rb +10 -0
- data/lib/amigrind/blueprints/provisioner.rb +20 -0
- data/lib/amigrind/blueprints/provisioners/file_upload.rb +29 -0
- data/lib/amigrind/blueprints/provisioners/local_shell.rb +28 -0
- data/lib/amigrind/blueprints/provisioners/remote_shell.rb +57 -0
- data/lib/amigrind/build/packer_runner.rb +106 -0
- data/lib/amigrind/build/rackerizer.rb +134 -0
- data/lib/amigrind/builder.rb +46 -0
- data/lib/amigrind/cli.rb +12 -0
- data/lib/amigrind/cli/_helpers.rb +49 -0
- data/lib/amigrind/cli/_root.rb +43 -0
- data/lib/amigrind/cli/blueprints/_category.rb +15 -0
- data/lib/amigrind/cli/blueprints/list.rb +21 -0
- data/lib/amigrind/cli/blueprints/show.rb +22 -0
- data/lib/amigrind/cli/build/_category.rb +15 -0
- data/lib/amigrind/cli/build/execute.rb +32 -0
- data/lib/amigrind/cli/build/print_packer.rb +28 -0
- data/lib/amigrind/cli/environments/_category.rb +15 -0
- data/lib/amigrind/cli/environments/list.rb +23 -0
- data/lib/amigrind/cli/environments/show.rb +22 -0
- data/lib/amigrind/cli/inventory/_category.rb +15 -0
- data/lib/amigrind/cli/inventory/add_to_channel.rb +28 -0
- data/lib/amigrind/cli/inventory/get-image.rb +34 -0
- data/lib/amigrind/cli/inventory/list.rb +14 -0
- data/lib/amigrind/cli/inventory/remove_from_channel.rb +28 -0
- data/lib/amigrind/cli/repo/_category.rb +15 -0
- data/lib/amigrind/cli/repo/init.rb +22 -0
- data/lib/amigrind/config.rb +89 -0
- data/lib/amigrind/environments/channel.rb +9 -0
- data/lib/amigrind/environments/environment.rb +66 -0
- data/lib/amigrind/environments/rb_evaluator.rb +7 -0
- data/lib/amigrind/repo.rb +217 -0
- data/lib/amigrind/version.rb +3 -0
- data/sample_config/config.yaml +9 -0
- data/sample_repo/.amigrind_root +0 -0
- data/sample_repo/blueprints/dependent_ubuntu.rb +21 -0
- data/sample_repo/blueprints/simple_ubuntu.rb +37 -0
- data/sample_repo/environments/development.yaml.example +15 -0
- 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,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,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,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
|