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