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.
- 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
|