stax 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/bin/stax +1 -4
  3. data/lib/stax.rb +33 -3
  4. data/lib/stax/asg.rb +140 -0
  5. data/lib/stax/aws/alb.rb +28 -0
  6. data/lib/stax/aws/asg.rb +34 -0
  7. data/lib/stax/aws/cfn.rb +102 -0
  8. data/lib/stax/aws/dynamodb.rb +27 -0
  9. data/lib/stax/aws/ec2.rb +22 -0
  10. data/lib/stax/aws/ecr.rb +25 -0
  11. data/lib/stax/aws/ecs.rb +54 -0
  12. data/lib/stax/aws/elb.rb +30 -0
  13. data/lib/stax/aws/emr.rb +28 -0
  14. data/lib/stax/aws/iam.rb +19 -0
  15. data/lib/stax/aws/keypair.rb +26 -0
  16. data/lib/stax/aws/kms.rb +19 -0
  17. data/lib/stax/aws/lambda.rb +33 -0
  18. data/lib/stax/aws/logs.rb +25 -0
  19. data/lib/stax/aws/s3.rb +41 -0
  20. data/lib/stax/aws/sdk.rb +21 -0
  21. data/lib/stax/aws/sg.rb +42 -0
  22. data/lib/stax/aws/sqs.rb +30 -0
  23. data/lib/stax/aws/ssm.rb +49 -0
  24. data/lib/stax/aws/sts.rb +31 -0
  25. data/lib/stax/base.rb +92 -4
  26. data/lib/stax/cfer.rb +59 -0
  27. data/lib/stax/cli.rb +13 -4
  28. data/lib/stax/docker.rb +106 -0
  29. data/lib/stax/dsl.rb +15 -0
  30. data/lib/stax/git.rb +61 -0
  31. data/lib/stax/github.rb +25 -0
  32. data/lib/stax/iam.rb +18 -0
  33. data/lib/stax/keypair.rb +75 -0
  34. data/lib/stax/mixin/alb.rb +45 -0
  35. data/lib/stax/mixin/asg.rb +115 -0
  36. data/lib/stax/mixin/dynamodb.rb +62 -0
  37. data/lib/stax/mixin/ec2.rb +37 -0
  38. data/lib/stax/mixin/ecs.rb +114 -0
  39. data/lib/stax/mixin/elb.rb +42 -0
  40. data/lib/stax/mixin/emr.rb +69 -0
  41. data/lib/stax/mixin/keypair.rb +45 -0
  42. data/lib/stax/mixin/kms.rb +30 -0
  43. data/lib/stax/mixin/lambda.rb +76 -0
  44. data/lib/stax/mixin/logs.rb +94 -0
  45. data/lib/stax/mixin/s3.rb +76 -0
  46. data/lib/stax/mixin/sg.rb +95 -0
  47. data/lib/stax/mixin/sqs.rb +49 -0
  48. data/lib/stax/mixin/ssh.rb +52 -0
  49. data/lib/stax/mixin/ssm.rb +137 -0
  50. data/lib/stax/stack.rb +19 -35
  51. data/lib/stax/stack/cfn.rb +24 -0
  52. data/lib/stax/stack/crud.rb +92 -0
  53. data/lib/stax/stack/outputs.rb +24 -0
  54. data/lib/stax/stack/parameters.rb +24 -0
  55. data/lib/stax/stack/resources.rb +35 -0
  56. data/lib/stax/staxfile.rb +41 -0
  57. data/lib/stax/subcommand.rb +12 -0
  58. data/lib/stax/version.rb +1 -1
  59. data/stax.gemspec +6 -13
  60. 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
@@ -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
- include Awful::Short
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 ||= cfn_safe(stack_prefix + class_name.downcase)
13
+ @_stack_name ||= stack_prefix + class_name
24
14
  end
25
15
 
26
16
  def exists?
27
- cf(:exists, [stack_name], quiet: true)
17
+ Cfn.exists?(stack_name)
28
18
  end
29
- end
30
19
 
31
- desc 'exists', 'test if stack exists'
32
- def exists
33
- puts exists?.to_s
34
- end
20
+ def stack_status
21
+ Cfn.describe(stack_name).stack_status
22
+ end
35
23
 
36
- desc 'resources', 'show resources for stack'
37
- method_option :type, aliases: '-t', type: :string, default: nil, desc: 'filter by resource type'
38
- method_option :match, aliases: '-m', type: :string, default: nil, desc: 'filter by resource regex'
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
- desc 'events', 'show all events for stack'
44
- def events
45
- cf(:events, [stack_name])
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 'outputs', 'show stack output'
51
- def outputs
52
- cf(:outputs, [stack_name])
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