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
@@ -1,8 +1,36 @@
1
- require 'thor'
2
-
3
1
  module Stax
4
2
  class Base < Thor
3
+
5
4
  no_commands do
5
+ def app_name
6
+ @_app_name ||= options[:app].empty? ? nil : cfn_safe(options[:app])
7
+ end
8
+
9
+ def branch_name
10
+ @_branch_name ||= cfn_safe(options[:branch])
11
+ end
12
+
13
+ def stack_prefix
14
+ @_stack_prefix ||= [app_name, branch_name].compact.join('-') + '-'
15
+ end
16
+
17
+ ## find or create a stack object
18
+ def stack(id)
19
+ object = Stax.const_get(id.to_s.capitalize)
20
+ ObjectSpace.each_object(object).first || object.new([], options)
21
+ end
22
+
23
+ def ensure_stack(*stacks)
24
+ stacks.each do |s|
25
+ stack(s)&.exists? or fail_task("#{s} stack is required")
26
+ end
27
+ end
28
+
29
+ ## alias for stack to preserve semantics
30
+ def command(id)
31
+ stack(id)
32
+ end
33
+
6
34
  def debug(message)
7
35
  say "[DEBUG] #{message}", :blue
8
36
  end
@@ -16,14 +44,74 @@ module Stax
16
44
  exit(1) if quit
17
45
  end
18
46
 
47
+ ## return true only if all given env vars are set
48
+ def env_set?(*vars)
49
+ vars.map{ |v| ENV.has_key?(v) }.all?
50
+ end
51
+
52
+ ## fail unless given env vars are set
53
+ def ensure_env(*vars)
54
+ unless env_set?(*vars)
55
+ fail_task("Please set env: #{vars.join(' ')}")
56
+ end
57
+ end
58
+
59
+ def color(string, hash)
60
+ set_color(string, hash.fetch(string.to_sym, :yellow))
61
+ end
62
+
19
63
  ## make string safe to use in naming CFN stuff
20
64
  def cfn_safe(string)
21
65
  string.gsub(/[\W_]/, '-')
22
66
  end
23
67
 
24
- def stack_prefix
25
- 'ops-'
68
+ def prepend(prefix, id)
69
+ p = prefix.to_s
70
+ id.start_with?(p) ? id : p + id
71
+ end
72
+
73
+ def append(suffix, id)
74
+ s = suffix.to_s
75
+ id.end_with?(s) ? id : id + s
76
+ end
77
+
78
+ def stringify_keys(thing)
79
+ if thing.is_a?(Hash)
80
+ Hash[ thing.map { |k,v| [ k.to_s, stringify_keys(v) ] } ]
81
+ elsif thing.respond_to?(:map)
82
+ thing.map { |v| stringify_keys(v) }
83
+ else
84
+ thing
85
+ end
86
+ end
87
+
88
+ ## psycho version of thor yes?() that demands a y or n answer
89
+ def y_or_n?(statement, color = nil)
90
+ loop do
91
+ case ask(statement, color, :add_to_history => false).downcase
92
+ when 'y'
93
+ return true
94
+ when 'n'
95
+ return false
96
+ else
97
+ puts "please respond 'y' or 'n'"
98
+ end
99
+ end
100
+ end
101
+
102
+ ## make epoch human-readable
103
+ def human_time(timestamp)
104
+ timestamp.nil? ? '-' : Time.at(timestamp.to_i/1000)
105
+ end
106
+
107
+ ## convert bytes to nearest unit
108
+ def human_bytes(bytes, precision = 0)
109
+ return 0.to_s if bytes < 1
110
+ {T: 1000*1000*1000*1000, G: 1000*1000*1000, M: 1000*1000, K: 1000, B: 1}.each do |unit, value|
111
+ return "#{(bytes.to_f/value).round(precision)}#{unit}" if bytes >= value
112
+ end
26
113
  end
114
+
27
115
  end
28
116
  end
29
117
  end
@@ -0,0 +1,59 @@
1
+ require 'cfer'
2
+
3
+ module Stax
4
+ class Stack < Base
5
+ class_option :use_previous_value, aliases: '-u', type: :array, default: [], desc: 'params to use previous value'
6
+
7
+ no_commands do
8
+ def cfer_parameters
9
+ {}
10
+ end
11
+
12
+ ## location of cfer template file
13
+ def cfer_template
14
+ File.join('cf', "#{class_name}.rb")
15
+ end
16
+
17
+ ## override with S3 bucket for upload of large templates as needed
18
+ def cfer_s3_path
19
+ nil
20
+ end
21
+
22
+ ## override with SNS ARNs as needed
23
+ def cfer_notification_arns
24
+ []
25
+ end
26
+
27
+ def cfer_termination_protection
28
+ false
29
+ end
30
+
31
+ ## create/update the stack
32
+ def cfer_converge(args = {})
33
+ opts = {
34
+ parameters: stringify_keys(cfer_parameters).except(*options[:use_previous_value]),
35
+ template: cfer_template,
36
+ follow: true,
37
+ number: 1,
38
+ s3_path: cfer_s3_path,
39
+ notification_arns: cfer_notification_arns,
40
+ enable_termination_protection: cfer_termination_protection,
41
+ }
42
+ Cfer.converge!(stack_name, opts.merge(args))
43
+ end
44
+
45
+ ## generate JSON for stack without sending to cloudformation
46
+ def cfer_generate
47
+ opts = {parameters: stringify_keys(cfer_parameters)}
48
+ Cfer.generate!(cfer_template, opts)
49
+ end
50
+
51
+ def cfer_tail
52
+ Cfer.tail!(stack_name, follow: true, number: 1)
53
+ rescue ::Aws::CloudFormation::Errors::ValidationError => e
54
+ puts e.message
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -1,6 +1,11 @@
1
+ require 'stax/aws/cfn'
2
+
1
3
  module Stax
2
4
  class Cli < Base
3
- include Awful::Short
5
+ include Aws
6
+
7
+ class_option :branch, type: :string, default: Git.branch, desc: 'git branch to use'
8
+ class_option :app, type: :string, default: File.basename(Git.toplevel), desc: 'application name'
4
9
 
5
10
  desc 'version', 'show version'
6
11
  def version
@@ -8,9 +13,13 @@ module Stax
8
13
  end
9
14
 
10
15
  desc 'ls', 'list stacks for this branch'
11
- def ls(regex = nil)
12
- # regex = cfn_safe(options[:branch]) + '(-|$)'
13
- cf(:ls, [regex || stack_prefix].compact, long: true)
16
+ def ls
17
+ print_table Cfn.stacks.select { |s|
18
+ s.stack_name.start_with?(stack_prefix)
19
+ }.map { |s|
20
+ [s.stack_name, s.creation_time, color(s.stack_status, Cfn::COLORS), s.template_description]
21
+ }
14
22
  end
23
+
15
24
  end
16
25
  end
@@ -0,0 +1,106 @@
1
+ require 'stax/aws/sts'
2
+ require 'stax/aws/ecr'
3
+
4
+ module Stax
5
+ class Docker < Base
6
+
7
+ no_commands do
8
+ ## default to ECR registry for this account
9
+ def docker_registry
10
+ @_docker_registry ||= "#{Aws::Sts.id.account}.dkr.ecr.#{ENV['AWS_REGION']}.amazonaws.com"
11
+ end
12
+
13
+ ## name the docker repo after the git repo
14
+ def docker_repository
15
+ @_docker_repository ||= File.basename(Git.toplevel)
16
+ end
17
+
18
+ ## full image name for docker push
19
+ def docker_image
20
+ @_docker_image ||= "#{docker_registry}/#{docker_repository}"
21
+ end
22
+
23
+ ## build a docker image locally
24
+ def docker_local_build
25
+ debug("Docker build #{docker_image}")
26
+ system "docker build -t #{docker_image} #{Git.toplevel}"
27
+ end
28
+
29
+ ## push docker image from local
30
+ def docker_push
31
+ debug("Docker push #{docker_image}")
32
+ system "docker push #{docker_image}"
33
+ end
34
+
35
+ ## override this for your argus setup
36
+ def docker_argus_queue
37
+ @_docker_argus_queue ||= Aws::Sqs.queue_url('argus.fifo')
38
+ end
39
+
40
+ def docker_argus_build
41
+ debug("Sending to argus #{Git.branch}:#{Git.sha}")
42
+ org, repo = Git.repo.split('/')
43
+ Aws::Sqs.send(
44
+ queue_url: docker_argus_queue,
45
+ message_group_id: repo,
46
+ message_body: {
47
+ org: org,
48
+ repo: repo,
49
+ branch: Git.branch,
50
+ sha: Git.sha,
51
+ }.to_json
52
+ ).tap do |r|
53
+ puts r&.message_id
54
+ end
55
+ end
56
+ end
57
+
58
+ desc 'registry', 'show registry name'
59
+ def registry
60
+ puts docker_registry
61
+ end
62
+
63
+ desc 'repository', 'show repository name'
64
+ def repository
65
+ puts docker_repository
66
+ end
67
+
68
+ desc 'image', 'show image name'
69
+ def image
70
+ puts docker_image
71
+ end
72
+
73
+ ## override this method with the desired builder
74
+ desc 'build', 'build docker image'
75
+ def build
76
+ docker_local_build
77
+ # docker_argus_build
78
+ end
79
+
80
+ desc 'login', 'login to registry'
81
+ def login
82
+ Aws::Ecr.auth.each do |auth|
83
+ debug("Login to ECR registry #{auth.proxy_endpoint}")
84
+ user, pass = Base64.decode64(auth.authorization_token).split(':')
85
+ system "docker login -u #{user} -p #{pass} #{auth.proxy_endpoint}"
86
+ end
87
+ end
88
+
89
+ desc 'push', 'push docker image to registry'
90
+ def push
91
+ docker_push
92
+ end
93
+
94
+ desc 'exists', 'check if docker image exists in ECR'
95
+ def exists
96
+ puts Aws::Ecr.exists?(docker_repository, Git.sha)
97
+ end
98
+
99
+ desc 'poll', 'poll ECR until docker image exists'
100
+ def poll
101
+ debug("Waiting for image in ECR #{docker_repository}:#{Git.sha}")
102
+ sleep 10 until Aws::Ecr.exists?(docker_repository, Git.sha)
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,15 @@
1
+ ## Staxfile DSL commands
2
+ module Stax
3
+ module Dsl
4
+ def stack(*args)
5
+ Stax.add_stack(*args)
6
+ end
7
+
8
+ def command(*args)
9
+ Stax.add_command(*args)
10
+ end
11
+ end
12
+ end
13
+
14
+ ## add main object singleton methods
15
+ extend Stax::Dsl
@@ -0,0 +1,61 @@
1
+ require 'git_clone_url'
2
+
3
+ module Stax
4
+ class Git < Base
5
+
6
+ no_commands do
7
+ def self.branch
8
+ @_branch ||= `git symbolic-ref --short HEAD`.chomp
9
+ end
10
+
11
+ def self.sha
12
+ @_sha ||= `git rev-parse HEAD`.chomp
13
+ end
14
+
15
+ def self.short_sha
16
+ @_short_sha ||= self.sha.slice(0,7)
17
+ end
18
+
19
+ def self.origin_url
20
+ @_origin_url ||= `git config --get remote.origin.url`.chomp
21
+ end
22
+
23
+ ## path like org/repo
24
+ def self.repo
25
+ @_repo ||= GitCloneUrl.parse(origin_url)&.path&.sub(/\.git$/, '')
26
+ end
27
+
28
+ def self.origin_sha
29
+ @_origin_sha ||= `git rev-parse origin/#{branch}`.chomp
30
+ end
31
+
32
+ def self.toplevel
33
+ @_toplevel ||= `git rev-parse --show-toplevel`.chomp
34
+ end
35
+
36
+ def self.uncommitted_changes?
37
+ !`git diff --shortstat`.chomp.empty?
38
+ end
39
+
40
+ def self.unpushed_commits?
41
+ sha != origin_sha
42
+ end
43
+
44
+ ## tag the sha and push to origin
45
+ def self.tag(tag, sha)
46
+ system "git tag #{tag} #{sha} && git push origin #{tag} --quiet"
47
+ end
48
+ end
49
+
50
+ desc 'branch', 'show current git branch'
51
+ def branch
52
+ puts Git.branch
53
+ end
54
+
55
+ desc 'sha', 'show current local git sha'
56
+ def sha
57
+ puts Git.sha
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,25 @@
1
+ require 'octokit'
2
+
3
+ module Stax
4
+ class Github < Base
5
+
6
+ no_commands do
7
+ def self.octokit
8
+ abort('Please set GITHUB_TOKEN') unless ENV['GITHUB_TOKEN']
9
+ @_octokit ||= Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
10
+ end
11
+
12
+ def self.tags
13
+ @_tags ||= octokit.tags(Git.repo)
14
+ end
15
+
16
+ ## check if this sha exists in github
17
+ def self.exists?
18
+ !octokit.commit(Git.repo, Git.sha).nil?
19
+ rescue Octokit::NotFound
20
+ false
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ require 'stax/aws/iam'
2
+ require 'stax/aws/sts'
3
+
4
+ module Stax
5
+ class Iam < Base
6
+
7
+ desc 'id', 'get account id'
8
+ def id
9
+ puts Aws::Sts.account_id
10
+ end
11
+
12
+ desc 'aliases', 'get account aliases'
13
+ def aliases
14
+ puts Aws::Iam.aliases
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ require 'tempfile'
2
+ require 'awful/keypair'
3
+ require 'awful/param'
4
+
5
+ module Stax
6
+ module Keypair
7
+ def self.included(thor)
8
+ thor.class_eval do
9
+
10
+ no_commands do
11
+ def key_pair_name
12
+ @_key_pair_name ||= stack_name
13
+ end
14
+
15
+ ## parameter store name to store private key
16
+ def key_pair_store_name
17
+ "#{key_pair_name}.key_pair"
18
+ end
19
+
20
+ def key_pair_describe
21
+ keypair(:ls, [key_pair_name], long: true)
22
+ rescue Aws::EC2::Errors::InvalidKeyPairNotFound => e
23
+ warn(e.message)
24
+ end
25
+
26
+ ## create a new key pair and return private key
27
+ def key_pair_create
28
+ keypair(:create, [key_pair_name], quiet: true).key_material
29
+ rescue Aws::EC2::Errors::InvalidKeyPairDuplicate => e
30
+ warn(e.message)
31
+ nil
32
+ end
33
+
34
+ ## create a key and store it in parameter store
35
+ def key_pair_store
36
+ key = key_pair_create or return
37
+ param(:put, [key_pair_store_name], value: key, type: 'SecureString', key_id: try(:kms_id), overwrite: true)
38
+ end
39
+
40
+ ## get private key from store and write to a tempfile for ssh to find; return file object
41
+ def key_pair_get
42
+ Tempfile.new(stack_name).tap do |file|
43
+ key = param(:get, [key_pair_store_name], decrypt: true, quiet: true).first
44
+ File.chmod(0400, file.path) # ssh needs this mode
45
+ file.write(key.value)
46
+ file.close
47
+ end
48
+ end
49
+
50
+ ## delete the key pair and the parameter store
51
+ def key_pair_delete
52
+ keypair(:delete, [key_pair_name], yes: true)
53
+ param(:delete, [key_pair_store_name], yes: true)
54
+ rescue Aws::SSM::Errors::ParameterNotFound
55
+ warn("Parameter #{key_pair_store_name} does not exist")
56
+ end
57
+ end
58
+
59
+ desc 'key', 'key pair tasks'
60
+ method_option :create, type: :boolean, default: false, desc: 'create a new key pair'
61
+ method_option :delete, type: :boolean, default: false, desc: 'delete key pair'
62
+ def key
63
+ if options[:create]
64
+ key_pair_store
65
+ elsif options[:delete]
66
+ key_pair_delete
67
+ else
68
+ key_pair_describe
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end