stax 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,12 @@ module Stax
14
14
  client.describe_table(table_name: name).table
15
15
  end
16
16
 
17
+ def global_table(name)
18
+ client.describe_global_table(global_table_name: name)&.global_table_description
19
+ rescue ::Aws::DynamoDB::Errors::GlobalTableNotFoundException
20
+ nil
21
+ end
22
+
17
23
  def gsi(name)
18
24
  client.describe_table(table_name: name).table.global_secondary_indexes || []
19
25
  end
@@ -71,6 +77,10 @@ module Stax
71
77
  end
72
78
  end
73
79
 
80
+ def put(opt)
81
+ client.put_item(opt)
82
+ end
83
+
74
84
  def list_backups(opt = {})
75
85
  last_arn = nil
76
86
  backups = []
@@ -16,7 +16,18 @@ module Stax
16
16
  end.map(&:instances).flatten
17
17
  end
18
18
 
19
+ ## list AMIs
20
+ def images(opt = {})
21
+ client.describe_images(opt).images.sort_by(&:creation_date)
22
+ end
23
+
24
+ ## tag AMIs
25
+ def create_tags(opt)
26
+ client.create_tags(opt)
27
+ end
28
+
19
29
  end
30
+
20
31
  end
21
32
  end
22
33
  end
@@ -14,10 +14,27 @@ module Stax
14
14
  client.get_authorization_token.authorization_data
15
15
  end
16
16
 
17
+ def repositories(opt = {})
18
+ paginate(:repositories) do |next_token|
19
+ client.describe_repositories(opt.merge(next_token: next_token))
20
+ end
21
+ end
22
+
17
23
  def exists?(repo, tag)
18
24
  !client.batch_get_image(repository_name: repo, image_ids: [{image_tag: tag}]).images.empty?
19
25
  end
20
26
 
27
+ def login(*registry_ids)
28
+ ids = registry_ids.empty? ? nil : Array(registry_ids)
29
+ client.get_authorization_token(registry_ids: ids).authorization_data
30
+ end
31
+
32
+ def images(opt = {})
33
+ paginate(:image_details) do |next_token|
34
+ client.describe_images(opt.merge(next_token: next_token))
35
+ end
36
+ end
37
+
21
38
  end
22
39
 
23
40
  end
@@ -24,18 +24,18 @@ module Stax
24
24
  client.describe_task_definition(task_definition: name).task_definition
25
25
  end
26
26
 
27
- def list_tasks(cluster, status = :RUNNING)
27
+ def list_tasks(opt)
28
28
  paginate(:task_arns) do |token|
29
- client.list_tasks(cluster: cluster, next_token: token, desired_status: status)
29
+ client.list_tasks(opt.merge(next_token: token))
30
30
  end
31
31
  end
32
32
 
33
- def tasks(cluster, status = :RUNNING)
34
- tasks = list_tasks(cluster, status)
33
+ def tasks(opt = {})
34
+ tasks = list_tasks(opt)
35
35
  if tasks.empty?
36
36
  []
37
37
  else
38
- client.describe_tasks(cluster: cluster, tasks: tasks).tasks
38
+ client.describe_tasks(cluster: opt[:cluster], tasks: tasks).tasks
39
39
  end
40
40
  end
41
41
 
@@ -0,0 +1,51 @@
1
+ module Stax
2
+ module Aws
3
+ class Route53 < Sdk
4
+
5
+ class << self
6
+
7
+ def client
8
+ @_client ||= ::Aws::Route53::Client.new
9
+ end
10
+
11
+ ## list all zones
12
+ def zones
13
+ client.list_hosted_zones.hosted_zones
14
+ end
15
+
16
+ ## list limited number of zones, starting at named zone
17
+ def zones_by_name(name, max_items = nil)
18
+ client.list_hosted_zones_by_name(
19
+ dns_name: name,
20
+ max_items: max_items,
21
+ )&.hosted_zones
22
+ end
23
+
24
+ ## get single matching zone, or nil
25
+ def zone_by_name(name)
26
+ zones_by_name(name, 1).find do |zone|
27
+ zone.name == name
28
+ end
29
+ end
30
+
31
+ ## record sets for named zone
32
+ def record_sets(opt = {})
33
+ client.list_resource_record_sets(opt)&.resource_record_sets
34
+ end
35
+
36
+ def record(name, type = :A)
37
+ zone = name.split('.').last(2).join('.') + '.'
38
+ Aws::Route53.record_sets(
39
+ hosted_zone_id: zone_by_name(zone).id,
40
+ start_record_name: name,
41
+ start_record_type: type,
42
+ ).select do |record|
43
+ (record.name == name) && (record.type == type.to_s)
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,5 @@
1
+ require 'stax/aws/sts'
2
+
1
3
  module Stax
2
4
  class Base < Thor
3
5
 
@@ -14,6 +16,14 @@ module Stax
14
16
  @_stack_prefix ||= [app_name, branch_name].compact.join('-') + '-'
15
17
  end
16
18
 
19
+ def aws_account_id
20
+ @_aws_account_id ||= Aws::Sts.id.account
21
+ end
22
+
23
+ def aws_region
24
+ @_aws_region ||= ENV['AWS_REGION']
25
+ end
26
+
17
27
  ## find or create a stack object
18
28
  def stack(id)
19
29
  object = Stax.const_get(id.to_s.capitalize)
@@ -104,6 +114,14 @@ module Stax
104
114
  timestamp.nil? ? '-' : Time.at(timestamp.to_i/1000)
105
115
  end
106
116
 
117
+ ## convert a diff in seconds to d h m s
118
+ def human_time_diff(t, n = 5)
119
+ mm, ss = t.divmod(60)
120
+ hh, mm = mm.divmod(60)
121
+ dd, hh = hh.divmod(24)
122
+ {d: dd, h: hh, m: mm, s: ss}.reject{ |_,v| v == 0 }.map{ |k,v| "#{v.round}#{k}" }.first(n).join
123
+ end
124
+
107
125
  ## convert bytes to nearest unit
108
126
  def human_bytes(bytes, precision = 0)
109
127
  return 0.to_s if bytes < 1
@@ -1,18 +1,45 @@
1
1
  require 'cfer'
2
2
 
3
+ ## TODO: remove these hacks once merged and released in upstream cfer
4
+ ## see cfer PRs: #52, #54
5
+ module Cfer::Core::Functions
6
+ def get_azs(region = '')
7
+ {"Fn::GetAZs" => region}
8
+ end
9
+
10
+ def cidr(ip_block, count, size_mask)
11
+ {"Fn::Cidr" => [ip_block, count, size_mask]}
12
+ end
13
+
14
+ def import_value(value)
15
+ {"Fn::ImportValue" => value}
16
+ end
17
+
18
+ def split(*args)
19
+ {"Fn::Split" => [ *args ].flatten }
20
+ end
21
+ end
22
+
23
+ ## see cfer PR: #56
24
+ module Cfer::Core
25
+ class Stack < Cfer::Block
26
+ def output(name, value, options = {})
27
+ opt = options.each_with_object({}) { |(k,v),h| h[k.to_s.capitalize] = v } # capitalize all keys
28
+ export = opt.has_key?('Export') ? {'Name' => opt['Export']} : nil
29
+ self[:Outputs][name] = opt.merge('Value' => value, 'Export' => export).compact
30
+ end
31
+ end
32
+ end
33
+
3
34
  module Stax
4
35
  class Stack < Base
5
36
  class_option :use_previous_value, aliases: '-u', type: :array, default: [], desc: 'params to use previous value'
6
37
 
7
38
  no_commands do
8
39
 
40
+ ## backward-compatibility
9
41
  def cfer_parameters
10
- {}
11
- end
12
-
13
- ## location of cfer template file
14
- def cfer_template
15
- File.join('cf', "#{class_name}.rb")
42
+ cfn_parameters
16
43
  end
17
44
 
18
45
  ## override with S3 bucket for upload of large templates as needed
@@ -29,37 +56,20 @@ module Stax
29
56
  false
30
57
  end
31
58
 
32
- ## create/update the stack
33
- def cfer_converge(args = {})
34
- opts = {
35
- parameters: stringify_keys(cfer_parameters).except(*options[:use_previous_value]),
36
- template: cfer_template,
37
- follow: true,
38
- number: 1,
39
- s3_path: cfer_s3_path,
40
- notification_arns: cfer_notification_arns,
41
- enable_termination_protection: cfer_termination_protection,
42
- }
43
- Cfer.converge!(stack_name, opts.merge(args))
44
- end
45
-
46
- ## generate JSON for stack without sending to cloudformation
47
- def cfer_generate
48
- opts = {parameters: stringify_keys(cfer_parameters)}
49
- Cfer.generate!(cfer_template, opts)
59
+ ## location of template file
60
+ def cfn_template_path
61
+ File.join('cf', "#{class_name}.rb")
50
62
  end
51
63
 
52
- ## generate method does puts, so steal stdout into a string
53
- def cfer_generate_string
54
- capture_stdout do
55
- cfer_generate
56
- end
64
+ def cfer_client
65
+ @_cfer_client ||= Cfer::Cfn::Client.new({})
57
66
  end
58
67
 
59
- def cfer_tail
60
- Cfer.tail!(stack_name, follow: true, number: 1)
61
- rescue ::Aws::CloudFormation::Errors::ValidationError => e
62
- puts e.message
68
+ ## generate JSON for stack without sending to cloudformation
69
+ def cfer_generate
70
+ Cfer::stack_from_file(cfn_template_path, client: cfer_client, parameters: stringify_keys(cfn_parameters)).to_json
71
+ rescue Cfer::Util::FileDoesNotExistError => e
72
+ fail_task(e.message)
63
73
  end
64
74
 
65
75
  ## temporarily grab stdout to a string
@@ -2,8 +2,6 @@ require 'stax/aws/cfn'
2
2
 
3
3
  module Stax
4
4
  class Cli < Base
5
- include Aws
6
-
7
5
  class_option :branch, type: :string, default: Git.branch, desc: 'git branch to use'
8
6
  class_option :app, type: :string, default: File.basename(Git.toplevel), desc: 'application name'
9
7
 
@@ -14,11 +12,11 @@ module Stax
14
12
 
15
13
  desc 'ls', 'list stacks for this branch'
16
14
  def ls
17
- print_table Cfn.stacks.select { |s|
15
+ print_table Aws::Cfn.stacks.select { |s|
18
16
  s.stack_name.start_with?(stack_prefix)
19
17
  }.map { |s|
20
- [s.stack_name, s.creation_time, color(s.stack_status, Cfn::COLORS), s.template_description]
21
- }
18
+ [s.stack_name, s.creation_time, color(s.stack_status, Aws::Cfn::COLORS), s.template_description]
19
+ }.sort
22
20
  end
23
21
 
24
22
  end
@@ -24,6 +24,24 @@ module Stax
24
24
  end
25
25
  end
26
26
 
27
+ desc 'update', 'meta update task'
28
+ def update
29
+ stack_objects.each do |s|
30
+ if s.exists? && y_or_n?("Update #{s.stack_name}?", :yellow)
31
+ s.update
32
+ end
33
+ end
34
+ end
35
+
36
+ desc 'change', 'meta change task'
37
+ def change
38
+ stack_objects.each do |s|
39
+ if s.exists?
40
+ s.change
41
+ end
42
+ end
43
+ end
44
+
27
45
  desc 'delete', 'meta delete task'
28
46
  def delete
29
47
  stack_objects.reverse.each do |s|
@@ -102,7 +102,7 @@ module Stax
102
102
  method_option :min_size, aliases: '-m', type: :numeric, default: nil, desc: 'set minimum capacity'
103
103
  method_option :max_size, aliases: '-M', type: :numeric, default: nil, desc: 'set maximum capacity'
104
104
  def scale
105
- opt = options.slice(:desired_capacity, :min_size, :max_size)
105
+ opt = options.slice(*%w[desired_capacity min_size max_size])
106
106
  fail_task('No change requested') if opt.empty?
107
107
  stack_asgs.each do |a|
108
108
  debug("Scaling to #{opt} for #{a.logical_resource_id} #{a.physical_resource_id}")
@@ -0,0 +1,98 @@
1
+ require 'stax/aws/codebuild'
2
+
3
+ module Stax
4
+ module Codebuild
5
+ def self.included(thor)
6
+ thor.desc(:codebuild, 'Codebuild subcommands')
7
+ thor.subcommand(:codebuild, Cmd::Codebuild)
8
+ end
9
+
10
+ def stack_projects
11
+ @_stack_projects ||= Aws::Cfn.resources_by_type(stack_name, 'AWS::CodeBuild::Project')
12
+ end
13
+
14
+ def stack_project_names
15
+ @_stack_project_names ||= stack_projects.map(&:physical_resource_id)
16
+ end
17
+ end
18
+
19
+ module Cmd
20
+ class Codebuild < SubCommand
21
+ COLORS = {
22
+ SUCCEEDED: :green,
23
+ FAILED: :red,
24
+ FAULT: :red,
25
+ CLIENT_ERROR: :red,
26
+ STOPPED: :red,
27
+ }
28
+
29
+ no_commands do
30
+ def print_phase(p)
31
+ duration = (d = p.duration_in_seconds) ? "#{d}s" : ''
32
+ status = p.phase_status || (p.phase_type == 'COMPLETED' ? '' : 'in progress')
33
+ puts "%-16s %-12s %4s %s" % [p.phase_type, color(status, COLORS), duration, p.end_time]
34
+ end
35
+ end
36
+
37
+ desc 'projects', 'list projects'
38
+ def projects
39
+ print_table Aws::Codebuild.projects(my.stack_project_names).map { |p|
40
+ [p.name, p.source.location, p.environment.image, p.environment.compute_type, p.last_modified]
41
+ }
42
+ end
43
+
44
+ desc 'builds', 'list builds for stack projects'
45
+ method_option :number, aliases: '-n', type: :numeric, default: 10, desc: 'number of builds to list'
46
+ def builds
47
+ my.stack_project_names.each do |project|
48
+ debug("Builds for #{project}")
49
+ ids = Aws::Codebuild.builds_for_project(project, options[:number])
50
+ print_table Aws::Codebuild.builds(ids).map { |b|
51
+ duration = human_time_diff(b.end_time - b.start_time)
52
+ [b.id, b.initiator, color(b.build_status, COLORS), duration, b.end_time]
53
+ }
54
+ end
55
+ end
56
+
57
+ desc 'phases [ID]', 'show build phases for given or most recent build'
58
+ def phases(id = nil)
59
+ id ||= Aws::Codebuild.builds_for_project(my.stack_project_names.first, 1).first
60
+ debug("Phases for build #{id}")
61
+ Aws::Codebuild.builds([id]).first.phases.each(&method(:print_phase))
62
+ end
63
+
64
+ desc 'tail [ID]', 'tail build phases for build'
65
+ def tail(id = nil)
66
+ trap('SIGINT', 'EXIT') # clean exit with ctrl-c
67
+ id ||= Aws::Codebuild.builds_for_project(my.stack_project_names.first, 1).first
68
+ debug("Phases for build #{id}")
69
+ seen = {}
70
+ loop do
71
+ (Aws::Codebuild.builds([id]).first.phases || []).each do |p|
72
+ i = p.phase_type + p.phase_status.to_s
73
+ print_phase(p) unless seen[i]
74
+ seen[i] = true
75
+ end
76
+ break if seen['COMPLETED']
77
+ sleep(3)
78
+ end
79
+ end
80
+
81
+ desc 'start', 'start a build'
82
+ method_option :project, type: :string, default: nil, desc: 'project to build'
83
+ method_option :version, type: :string, default: nil, desc: 'source version to build (sha/branch/tag)'
84
+ def start
85
+ project = options[:project] || my.stack_project_names.first
86
+ version = options[:version] || Git.sha
87
+ debug("Starting build for #{project} #{version}")
88
+ build = Aws::Codebuild.start(
89
+ project_name: project,
90
+ source_version: version,
91
+ )
92
+ puts build.id
93
+ tail build.id
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,125 @@
1
+ require 'stax/aws/codepipeline'
2
+
3
+ module Stax
4
+ module Codepipeline
5
+ def self.included(thor)
6
+ thor.desc(:codepipeline, 'Codepipeline subcommands')
7
+ thor.subcommand(:codepipeline, Cmd::Codepipeline)
8
+ end
9
+
10
+ def stack_pipelines
11
+ @_stack_pipelines ||= Aws::Cfn.resources_by_type(stack_name, 'AWS::CodePipeline::Pipeline')
12
+ end
13
+
14
+ def stack_pipeline_names
15
+ @_stack_pipeline_names ||= stack_pipelines.map(&:physical_resource_id)
16
+ end
17
+ end
18
+
19
+ module Cmd
20
+ class Codepipeline < SubCommand
21
+ COLORS = {
22
+ Succeeded: :green,
23
+ Failed: :red,
24
+ }
25
+
26
+ desc 'stages', 'list pipeline stages'
27
+ def stages
28
+ my.stack_pipeline_names.each do |name|
29
+ debug("Stages for #{name}")
30
+ print_table Aws::Codepipeline.stages(name).map { |s|
31
+ actions = s.actions.map{ |a| a&.action_type_id&.provider }.join(' ')
32
+ [s.name, actions]
33
+ }
34
+ end
35
+ end
36
+
37
+ desc 'history', 'pipeline execution history'
38
+ method_option :number, aliases: '-n', type: :numeric, default: 10, desc: 'number of items'
39
+ def history
40
+ my.stack_pipeline_names.each do |name|
41
+ debug("Execution history for #{name}")
42
+ print_table Aws::Codepipeline.executions(name, options[:number]).map { |e|
43
+ r = Aws::Codepipeline.execution(name, e.pipeline_execution_id)&.artifact_revisions&.first
44
+ age = human_time_diff(Time.now - e.last_update_time, 1)
45
+ duration = human_time_diff(e.last_update_time - e.start_time)
46
+ [e.pipeline_execution_id, color(e.status, COLORS), "#{age} ago", duration, r&.revision_id&.slice(0,7) + ':' + r&.revision_summary]
47
+ }
48
+ end
49
+ end
50
+
51
+ desc 'state', 'pipeline state'
52
+ def state
53
+ my.stack_pipeline_names.each do |name|
54
+ state = Aws::Codepipeline.state(name)
55
+ debug("State for #{name} at #{state.updated}")
56
+ print_table state.stage_states.map { |s|
57
+ s.action_states.map { |a|
58
+ l = a.latest_execution
59
+ percent = (l&.percent_complete || 100).to_s + '%'
60
+ sha = a.current_revision&.revision_id&.slice(0,7)
61
+ ago = (t = l&.last_status_change) ? human_time_diff(Time.now - t, 1) : '?'
62
+ [s.stage_name, a.action_name, color(l&.status || '', COLORS), percent, "#{ago} ago", (sha || l&.token), l&.error_details&.message]
63
+ }
64
+ }.flatten(1)
65
+ end
66
+ end
67
+
68
+ desc 'approvals', 'approve or reject pending approvals'
69
+ method_option :approved, type: :boolean, default: false, desc: 'approve the request'
70
+ method_option :rejected, type: :boolean, default: false, desc: 'reject the request'
71
+ def approvals
72
+ my.stack_pipeline_names.each do |name|
73
+ debug("Pending approvals for #{name}")
74
+ Aws::Codepipeline.state(name).stage_states.each do |s|
75
+ s.action_states.each do |a|
76
+ next unless (a.latest_execution&.token && a.latest_execution&.status == 'InProgress')
77
+ l = a.latest_execution
78
+ ago = (t = l&.last_status_change) ? human_time_diff(Time.now - t, 1) : '?'
79
+ puts "#{a.action_name} #{l&.token} #{ago} ago"
80
+ resp = (options[:approved] && :approved) || (options[:rejected] && :rejected) || ask('approved,rejected,[skip]?', :yellow)
81
+ status = resp.to_s.capitalize
82
+ if (status == 'Rejected') || (status == 'Approved')
83
+ Aws::Codepipeline.client.put_approval_result(
84
+ pipeline_name: name,
85
+ stage_name: s.stage_name,
86
+ action_name: a.action_name,
87
+ token: l.token,
88
+ result: {status: status, summary: "#{status} by #{ENV['USER']}"},
89
+ ).tap { |r| puts "#{status} at #{r&.approved_at}" }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ desc 'start [NAME]', 'start execution for pipeline'
97
+ def start(name = nil)
98
+ name ||= my.stack_pipeline_names.first
99
+ debug("Starting execution for #{name}")
100
+ puts Aws::Codepipeline.start(name)
101
+ tail name
102
+ end
103
+
104
+ desc 'tail [NAME]', 'tail pipeline state changes'
105
+ def tail(name = nil)
106
+ trap('SIGINT', 'EXIT') # clean exit with ctrl-c
107
+ name ||= my.stack_pipeline_names.first
108
+ last_seen = nil
109
+ loop do
110
+ state = Aws::Codepipeline.state(name)
111
+ now = Time.now
112
+ stages = state.stage_states.map do |s|
113
+ last_change = s.action_states.map { |a| a&.latest_execution&.last_status_change }.compact.max
114
+ revisions = s.action_states.map { |a| a.current_revision&.revision_id&.slice(0,7) }.join(' ')
115
+ ago = last_change ? human_time_diff(now - last_change, 1) : '?'
116
+ [s.stage_name, color(s&.latest_execution&.status || '', COLORS), "#{ago} ago", revisions].join(' ')
117
+ end
118
+ puts [set_color(now, :blue), stages].flatten.join(' ')
119
+ sleep 5
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end