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
data/lib/stax/base.rb
CHANGED
@@ -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
|
25
|
-
|
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
|
data/lib/stax/cfer.rb
ADDED
@@ -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
|
data/lib/stax/cli.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
+
require 'stax/aws/cfn'
|
2
|
+
|
1
3
|
module Stax
|
2
4
|
class Cli < Base
|
3
|
-
include
|
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
|
12
|
-
|
13
|
-
|
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
|
data/lib/stax/docker.rb
ADDED
@@ -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
|
data/lib/stax/dsl.rb
ADDED
data/lib/stax/git.rb
ADDED
@@ -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
|
data/lib/stax/github.rb
ADDED
@@ -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
|
data/lib/stax/iam.rb
ADDED
@@ -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
|
data/lib/stax/keypair.rb
ADDED
@@ -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
|