stax 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/stax +1 -4
- data/lib/stax.rb +33 -3
- data/lib/stax/asg.rb +140 -0
- data/lib/stax/aws/alb.rb +28 -0
- data/lib/stax/aws/asg.rb +34 -0
- data/lib/stax/aws/cfn.rb +102 -0
- data/lib/stax/aws/dynamodb.rb +27 -0
- data/lib/stax/aws/ec2.rb +22 -0
- data/lib/stax/aws/ecr.rb +25 -0
- data/lib/stax/aws/ecs.rb +54 -0
- data/lib/stax/aws/elb.rb +30 -0
- data/lib/stax/aws/emr.rb +28 -0
- data/lib/stax/aws/iam.rb +19 -0
- data/lib/stax/aws/keypair.rb +26 -0
- data/lib/stax/aws/kms.rb +19 -0
- data/lib/stax/aws/lambda.rb +33 -0
- data/lib/stax/aws/logs.rb +25 -0
- data/lib/stax/aws/s3.rb +41 -0
- data/lib/stax/aws/sdk.rb +21 -0
- data/lib/stax/aws/sg.rb +42 -0
- data/lib/stax/aws/sqs.rb +30 -0
- data/lib/stax/aws/ssm.rb +49 -0
- data/lib/stax/aws/sts.rb +31 -0
- data/lib/stax/base.rb +92 -4
- data/lib/stax/cfer.rb +59 -0
- data/lib/stax/cli.rb +13 -4
- data/lib/stax/docker.rb +106 -0
- data/lib/stax/dsl.rb +15 -0
- data/lib/stax/git.rb +61 -0
- data/lib/stax/github.rb +25 -0
- data/lib/stax/iam.rb +18 -0
- data/lib/stax/keypair.rb +75 -0
- data/lib/stax/mixin/alb.rb +45 -0
- data/lib/stax/mixin/asg.rb +115 -0
- data/lib/stax/mixin/dynamodb.rb +62 -0
- data/lib/stax/mixin/ec2.rb +37 -0
- data/lib/stax/mixin/ecs.rb +114 -0
- data/lib/stax/mixin/elb.rb +42 -0
- data/lib/stax/mixin/emr.rb +69 -0
- data/lib/stax/mixin/keypair.rb +45 -0
- data/lib/stax/mixin/kms.rb +30 -0
- data/lib/stax/mixin/lambda.rb +76 -0
- data/lib/stax/mixin/logs.rb +94 -0
- data/lib/stax/mixin/s3.rb +76 -0
- data/lib/stax/mixin/sg.rb +95 -0
- data/lib/stax/mixin/sqs.rb +49 -0
- data/lib/stax/mixin/ssh.rb +52 -0
- data/lib/stax/mixin/ssm.rb +137 -0
- data/lib/stax/stack.rb +19 -35
- data/lib/stax/stack/cfn.rb +24 -0
- data/lib/stax/stack/crud.rb +92 -0
- data/lib/stax/stack/outputs.rb +24 -0
- data/lib/stax/stack/parameters.rb +24 -0
- data/lib/stax/stack/resources.rb +35 -0
- data/lib/stax/staxfile.rb +41 -0
- data/lib/stax/subcommand.rb +12 -0
- data/lib/stax/version.rb +1 -1
- data/stax.gemspec +6 -13
- metadata +105 -9
@@ -0,0 +1,52 @@
|
|
1
|
+
## include this mixin in stack classes that need ssh to instances
|
2
|
+
## and consider defining methods: ssh_options, before_ssh, after_ssh
|
3
|
+
require 'stax/aws/ec2'
|
4
|
+
|
5
|
+
module Stax
|
6
|
+
module Ssh
|
7
|
+
def self.included(thor)
|
8
|
+
|
9
|
+
## stack class can define this
|
10
|
+
# def ssh_options
|
11
|
+
# {
|
12
|
+
# StrictHostKeyChecking: 'no',
|
13
|
+
# UserKnownHostsFile: '/dev/null',
|
14
|
+
# }
|
15
|
+
# end
|
16
|
+
|
17
|
+
## IP address to ssh
|
18
|
+
def ssh_instances
|
19
|
+
Aws::Ec2.instances(stack_name).map(&:public_ip_address)
|
20
|
+
end
|
21
|
+
|
22
|
+
def ssh_options_format(opt)
|
23
|
+
opt.reject do |_,v|
|
24
|
+
v.nil?
|
25
|
+
end.map do |k,v|
|
26
|
+
"-o #{k}=#{v}"
|
27
|
+
end.join(' ')
|
28
|
+
end
|
29
|
+
|
30
|
+
def ssh_cmd(instances, cmd = [], opt = {})
|
31
|
+
c = cmd.join(' ')
|
32
|
+
o = ssh_options_format((try(:ssh_options) || {}).merge(opt))
|
33
|
+
instances.each do |i|
|
34
|
+
system "ssh #{o} #{i} #{c}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
thor.class_eval do
|
39
|
+
|
40
|
+
## stack class can add before/after_ssh hooks
|
41
|
+
desc 'ssh [CMD]', 'ssh to ec2 instances'
|
42
|
+
def ssh(*cmd)
|
43
|
+
try(:before_ssh)
|
44
|
+
ssh_cmd(ssh_instances, cmd)
|
45
|
+
ensure
|
46
|
+
try(:after_ssh)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'stax/aws/ssm'
|
3
|
+
|
4
|
+
module Stax
|
5
|
+
module Ssm
|
6
|
+
|
7
|
+
def self.included(thor)
|
8
|
+
thor.desc(:ssm, 'SSM subcommands')
|
9
|
+
thor.subcommand(:ssm, Cmd::Ssm)
|
10
|
+
end
|
11
|
+
|
12
|
+
def ssm_parameter_path
|
13
|
+
@_ssm_parameter_path ||= prepend('/', [app_name, branch_name, class_name].join('/'))
|
14
|
+
end
|
15
|
+
|
16
|
+
def ssm_parameter_name(name)
|
17
|
+
[ssm_parameter_path, name].join('/')
|
18
|
+
end
|
19
|
+
|
20
|
+
def ssm_parameter_put(name, value, opt = {})
|
21
|
+
Aws::Ssm.put(
|
22
|
+
{
|
23
|
+
name: ssm_parameter_name(name),
|
24
|
+
value: value,
|
25
|
+
type: :SecureString,
|
26
|
+
# key_id: options[:key],
|
27
|
+
overwrite: true,
|
28
|
+
}.merge(opt)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ssm_parameter_get(name)
|
33
|
+
Aws::Ssm.get(names: [ssm_parameter_name(name)], with_decryption: true).first&.value
|
34
|
+
end
|
35
|
+
|
36
|
+
## get a parameter from the store to a Tmpfile
|
37
|
+
def ssm_parameter_tmpfile(name)
|
38
|
+
Tempfile.new(stack_name).tap do |file|
|
39
|
+
file.write(ssm_parameter_get(name))
|
40
|
+
File.chmod(0400, file.path)
|
41
|
+
file.close
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def ssm_parameter_delete(*names)
|
46
|
+
Aws::Ssm.delete(names: names.map { |name| ssm_parameter_name(name) })
|
47
|
+
end
|
48
|
+
|
49
|
+
## run a command on stack instances
|
50
|
+
def ssm_run_shellscript(*cmd)
|
51
|
+
Aws::Ssm.run(
|
52
|
+
document_name: 'AWS-RunShellScript',
|
53
|
+
targets: [{key: 'tag:aws:cloudformation:stack-name', values: [stack_name]}],
|
54
|
+
parameters: {commands: cmd}
|
55
|
+
)&.command_id.tap(&method(:puts))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module Cmd
|
60
|
+
class Ssm < SubCommand
|
61
|
+
|
62
|
+
COLORS = {
|
63
|
+
Online: :green,
|
64
|
+
ConnectionLost: :red,
|
65
|
+
}
|
66
|
+
|
67
|
+
desc 'instances', 'SSM instance agent information'
|
68
|
+
def instances
|
69
|
+
print_table Aws::Ssm.instances(my.stack_name).map { |i|
|
70
|
+
agent = set_color(i.agent_version, i.is_latest_version ? :green : :yellow)
|
71
|
+
[i.instance_id, color(i.ping_status, COLORS), i.last_ping_date_time, agent]
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
desc 'shellscript CMD', 'SSM run shell command'
|
76
|
+
def shellscript(*cmd)
|
77
|
+
my.ssm_run_shellscript(*cmd)
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'commands', 'list SSM commands'
|
81
|
+
def commands
|
82
|
+
print_table Aws::Ssm.commands.map { |c|
|
83
|
+
[
|
84
|
+
c.command_id,
|
85
|
+
c.document_name,
|
86
|
+
color(c.status, COLORS),
|
87
|
+
c.requested_date_time,
|
88
|
+
c.comment
|
89
|
+
]
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
desc 'invocation [ID]', 'details for given/latest invocation'
|
94
|
+
def invocation(id = nil)
|
95
|
+
id ||= Aws::Ssm.commands.first.command_id
|
96
|
+
Aws::Ssm.invocation(id).each do |i|
|
97
|
+
puts YAML.dump(stringify_keys(i.to_hash))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
desc 'parameters [PATH]', 'list parameters'
|
102
|
+
method_option :decrypt, aliases: '-d', type: :boolean, default: false, desc: 'decrypt and show values'
|
103
|
+
method_option :recurse, aliases: '-r', type: :boolean, default: false, desc: 'recurse path hierarchy'
|
104
|
+
def parameters(path = my.ssm_parameter_path)
|
105
|
+
fields = %i[name type]
|
106
|
+
fields << :value if options[:decrypt]
|
107
|
+
print_table Aws::Ssm.parameters(
|
108
|
+
path: path,
|
109
|
+
with_decryption: options[:decrypt],
|
110
|
+
recursive: options[:recurse],
|
111
|
+
).map { |p| fields.map{ |f| p.send(f) } }
|
112
|
+
end
|
113
|
+
|
114
|
+
desc 'get NAME', 'get parameter'
|
115
|
+
def get(name)
|
116
|
+
puts my.ssm_parameter_get(name)
|
117
|
+
end
|
118
|
+
|
119
|
+
desc 'put NAME VALUE', 'put parameter'
|
120
|
+
method_option :type, type: :string, default: :SecureString, desc: 'type of value'
|
121
|
+
method_option :key, type: :string, default: nil, desc: 'kms key'
|
122
|
+
method_option :overwrite, aliases: '-o', type: :boolean, default: false, desc: 'overwrite existing'
|
123
|
+
def put(name, value)
|
124
|
+
my.ssm_parameter_put(name, value, type: options[:type], key_id: options[:key], overwrite: options[:overwrite])
|
125
|
+
rescue ::Aws::SSM::Errors::ParameterAlreadyExists => e
|
126
|
+
warn(e.message)
|
127
|
+
end
|
128
|
+
|
129
|
+
desc 'delete NAMES', 'delete parameters'
|
130
|
+
def delete(*names)
|
131
|
+
puts my.ssm_parameter_delete(*names)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
data/lib/stax/stack.rb
CHANGED
@@ -1,55 +1,39 @@
|
|
1
1
|
module Stax
|
2
|
-
## add a Stack subclass as a thor subcommand
|
3
|
-
def self.add_stack(name)
|
4
|
-
c = name.capitalize
|
5
|
-
|
6
|
-
## create the class if it does not exist yet
|
7
|
-
klass = self.const_defined?(c) ? self.const_get(c) : self.const_set(c, Class.new(Stack))
|
8
|
-
|
9
|
-
## create thor subcommand
|
10
|
-
Cli.desc(name, "control #{name} stack")
|
11
|
-
Cli.subcommand(name, klass)
|
12
|
-
end
|
13
|
-
|
14
2
|
class Stack < Base
|
15
|
-
|
3
|
+
|
4
|
+
class_option :resources, type: :array, default: nil, desc: 'resources IDs to allow updates'
|
5
|
+
class_option :all, type: :boolean, default: false, desc: 'DANGER: allow updates to all resources'
|
16
6
|
|
17
7
|
no_commands do
|
18
8
|
def class_name
|
19
|
-
@_class_name ||= self.class.to_s.split('::').last
|
9
|
+
@_class_name ||= self.class.to_s.split('::').last.downcase
|
20
10
|
end
|
21
11
|
|
22
12
|
def stack_name
|
23
|
-
@_stack_name ||=
|
13
|
+
@_stack_name ||= stack_prefix + class_name
|
24
14
|
end
|
25
15
|
|
26
16
|
def exists?
|
27
|
-
|
17
|
+
Cfn.exists?(stack_name)
|
28
18
|
end
|
29
|
-
end
|
30
19
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
end
|
20
|
+
def stack_status
|
21
|
+
Cfn.describe(stack_name).stack_status
|
22
|
+
end
|
35
23
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
def resources
|
40
|
-
cf(:resources, [stack_name], options.merge(long: true))
|
41
|
-
end
|
24
|
+
def stack_notification_arns
|
25
|
+
Cfn.describe(stack_name).notification_arns
|
26
|
+
end
|
42
27
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
rescue Aws::CloudFormation::Errors::ValidationError => e
|
47
|
-
puts e.message
|
28
|
+
def resource(id)
|
29
|
+
Aws::Cfn.id(stack_name, id)
|
30
|
+
end
|
48
31
|
end
|
49
32
|
|
50
|
-
desc '
|
51
|
-
def
|
52
|
-
|
33
|
+
desc 'exists', 'test if stack exists'
|
34
|
+
def exists
|
35
|
+
puts exists?
|
53
36
|
end
|
37
|
+
|
54
38
|
end
|
55
39
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
include Aws
|
4
|
+
|
5
|
+
desc 'template', 'get template of existing stack from cloudformation'
|
6
|
+
method_option :pretty, type: :boolean, default: true, desc: 'format json output'
|
7
|
+
def template
|
8
|
+
Cfn.template(stack_name).tap { |t|
|
9
|
+
puts options[:pretty] ? JSON.pretty_generate(JSON.parse(t)) : t
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'events', 'show all events for stack'
|
14
|
+
method_option :number, aliases: '-n', type: :numeric, default: nil, desc: 'show n most recent events'
|
15
|
+
def events
|
16
|
+
print_table Cfn.events(stack_name).tap { |events|
|
17
|
+
events.replace(events.first(options[:number])) if options[:number]
|
18
|
+
}.reverse.map { |e|
|
19
|
+
[e.timestamp, color(e.resource_status, Cfn::COLORS), e.resource_type, e.logical_resource_id, e.resource_status_reason]
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
|
4
|
+
class_option :resources, type: :array, default: nil, desc: 'resources IDs to allow updates'
|
5
|
+
class_option :all, type: :boolean, default: false, desc: 'DANGER: allow updates to all resources'
|
6
|
+
|
7
|
+
no_commands do
|
8
|
+
## policy to lock the stack to all updates
|
9
|
+
def stack_policy
|
10
|
+
{
|
11
|
+
Statement: [
|
12
|
+
Effect: 'Deny',
|
13
|
+
Action: 'Update:*',
|
14
|
+
Principal: '*',
|
15
|
+
Resource: '*'
|
16
|
+
]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
## temporary policy during updates
|
21
|
+
def stack_policy_during_update
|
22
|
+
{
|
23
|
+
Statement: [
|
24
|
+
Effect: 'Allow',
|
25
|
+
Action: 'Update:*',
|
26
|
+
Principal: '*',
|
27
|
+
Resource: stack_update_resources
|
28
|
+
]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
## resources to unlock during update
|
33
|
+
def stack_update_resources
|
34
|
+
(options[:all] ? ['*'] : options[:resources]).map do |r|
|
35
|
+
"LogicalResourceId/#{r}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
## cleanup sometimes needs to wait
|
40
|
+
def wait_for_delete(seconds = 5)
|
41
|
+
return unless exists?
|
42
|
+
debug("Waiting for #{stack_name} to delete")
|
43
|
+
loop do
|
44
|
+
sleep(seconds)
|
45
|
+
break unless exists?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
desc 'create', 'create stack'
|
51
|
+
def create
|
52
|
+
fail_task("Stack #{stack_name} already exists") if exists?
|
53
|
+
debug("Creating stack #{stack_name}")
|
54
|
+
cfer_converge(stack_policy: stack_policy)
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'update', 'update stack'
|
58
|
+
def update
|
59
|
+
fail_task("Stack #{stack_name} does not exist") unless exists?
|
60
|
+
debug("Updating stack #{stack_name}")
|
61
|
+
cfer_converge(stack_policy_during_update: stack_policy_during_update)
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'delete', 'delete stack'
|
65
|
+
def delete
|
66
|
+
if yes? "Really delete stack #{stack_name}?", :yellow
|
67
|
+
Cfn.delete(stack_name)
|
68
|
+
end
|
69
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
70
|
+
fail_task(e.message)
|
71
|
+
end
|
72
|
+
|
73
|
+
desc 'tail', 'tail stack events'
|
74
|
+
def tail
|
75
|
+
cfer_tail
|
76
|
+
end
|
77
|
+
|
78
|
+
desc 'protection', 'show/set termination protection for stack'
|
79
|
+
method_option :enable, aliases: '-e', type: :boolean, default: nil, desc: 'enable termination protection'
|
80
|
+
method_option :disable, aliases: '-d', type: :boolean, default: nil, desc: 'disable termination protection'
|
81
|
+
def protection
|
82
|
+
if options[:enable]
|
83
|
+
Cfn.protection(stack_name, true)
|
84
|
+
elsif options[:disable]
|
85
|
+
Cfn.protection(stack_name, false)
|
86
|
+
end
|
87
|
+
debug("Termination protection for #{stack_name}")
|
88
|
+
puts Cfn.describe(stack_name)&.enable_termination_protection
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
|
4
|
+
no_commands do
|
5
|
+
def stack_outputs
|
6
|
+
@_stack_outputs ||= Cfn.outputs(stack_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def stack_output(key)
|
10
|
+
stack_outputs.fetch(key.to_s, nil)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'outputs', 'show stack outputs'
|
15
|
+
def outputs(key = nil)
|
16
|
+
if key
|
17
|
+
puts stack_output(key)
|
18
|
+
else
|
19
|
+
print_table stack_outputs
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
|
4
|
+
no_commands do
|
5
|
+
def stack_parameters
|
6
|
+
@_stack_parameters ||= Cfn.parameters(stack_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def stack_parameter(key)
|
10
|
+
stack_parameters.find do |p|
|
11
|
+
p.parameter_key == key.to_s
|
12
|
+
end&.parameter_value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'parameters', 'show stack input parameters'
|
17
|
+
def parameters
|
18
|
+
print_table stack_parameters.each_with_object({}) { |p, h|
|
19
|
+
h[p.parameter_key] = p.parameter_value
|
20
|
+
}.sort
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
|
4
|
+
no_commands do
|
5
|
+
def stack_resources
|
6
|
+
@_stack_resources ||= Cfn.resources(stack_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def stack_resources_by_type(type)
|
10
|
+
stack_resources.select do |r|
|
11
|
+
r.resource_type == type
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'resources', 'list resources for this stack'
|
17
|
+
method_option :match, aliases: '-m', type: :string, default: nil, desc: 'filter by resource regex'
|
18
|
+
def resources
|
19
|
+
print_table stack_resources.tap { |resources|
|
20
|
+
if options[:match]
|
21
|
+
m = Regexp.new(options[:match], Regexp::IGNORECASE)
|
22
|
+
resources.select! { |r| m.match(r.resource_type) }
|
23
|
+
end
|
24
|
+
}.map { |r|
|
25
|
+
[r.logical_resource_id, r.resource_type, color(r.resource_status, Cfn::COLORS), r.physical_resource_id]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'id [LOGICAL_ID]', 'get physical ID from resource logical ID'
|
30
|
+
def id(resource)
|
31
|
+
puts Cfn.id(stack_name, resource)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|