stax 0.0.1 → 0.0.2
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 +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
|