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