ufo 6.0.0 → 6.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f7705c4016b08dd205ad9209ee255b6cd3411fea731183b6c859b28cf900990
4
- data.tar.gz: 4eb918b33c3a87ed71fbc64e3a0dbac8766a02ccbf15d7e35b863a26f7fd78a3
3
+ metadata.gz: 2ddf01cabd60971596b36544d0e7e91484e935143d83fa1410eb6a5d29e06dff
4
+ data.tar.gz: 5bc24a9f42c06fb4ee486fbda776c2d8ffe4f7f35b3fce74f3837dfcded1c7b8
5
5
  SHA512:
6
- metadata.gz: c65f585f02f1a54afa5c3c2c0a36378e219b8490eec8321d1dd18f9cec4026108f39e11215d0ec964b1842dbca8ade66d4f7e0b256c41386f86eb0db60f931b2
7
- data.tar.gz: 16226423b75d58394e9e92dcd4562629e9defc5ed46a2acad4e0f049deca9e0c525e2ddaec984eaac616edebe996c1e8e154f3ea3fbb73ae8dfd108bd68958cf
6
+ metadata.gz: 8a0ca12fac2697a82bb8b2930a5d20eef0dba27ac7c9be684a221b271d765fd923fd32f91b5fb5bf7aeafb416a4479e42be770055575451bae5102c0aba64a54
7
+ data.tar.gz: fd9c668daf4d70bc37f6898ea224578cfd3d47e87aa3d691f903664083fe3fd7083dabb8b279a2082a3283e4fd8a3d83413806a0e6c208e1489362b81041ac6f
data/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [6.0.3] - 2022-03-07
7
+ - [#133](https://github.com/tongueroo/ufo/pull/133) improve ufo central and add helpers
8
+
9
+ ## [6.0.2] - 2022-03-06
10
+ - [#128](https://github.com/tongueroo/ufo/pull/128) cleanup region with aws_data
11
+ - [#129](https://github.com/tongueroo/ufo/pull/129) Scale and Ps Edge Cases
12
+ - [#130](https://github.com/tongueroo/ufo/pull/130) compiled yaml errors: print code with line number context
13
+ - [#131](https://github.com/tongueroo/ufo/pull/131) ufo central update symlink creation
14
+ - [#132](https://github.com/tongueroo/ufo/pull/132) ufo ps improvements better catchall error messages reporting
15
+
16
+ ## [6.0.1] - 2022-03-05
17
+ - [#126](https://github.com/tongueroo/ufo/pull/126) ecs deployment_configuration options
18
+ - [#127](https://github.com/tongueroo/ufo/pull/127) improve ps errors reporting
19
+
6
20
  ## [6.0.0] - 2022-03-05
7
21
  - [#125](https://github.com/tongueroo/ufo/pull/125) v6: major ufo upgrades and new structure
8
22
 
@@ -16,6 +16,8 @@ containerDefinitions:
16
16
  protocol: tcp
17
17
  <% end -%>
18
18
  command: <%= @command.to_json %>
19
+ linuxParameters:
20
+ initProcessEnabled: true
19
21
  environment: <%= @environment.to_json if @environment %>
20
22
  secrets: <%= @secrets.to_json if @secrets %>
21
23
  <% if @awslogs_group -%>
@@ -1,3 +1,4 @@
1
+ require "aws-sdk-acm"
1
2
  require "aws-sdk-applicationautoscaling"
2
3
  require "aws-sdk-cloudformation"
3
4
  require "aws-sdk-cloudwatchlogs"
@@ -14,6 +15,11 @@ module Ufo
14
15
  module AwsServices
15
16
  extend Memoist
16
17
 
18
+ def acm
19
+ Aws::ACM::Client.new(aws_options)
20
+ end
21
+ memoize :acm
22
+
17
23
  def applicationautoscaling
18
24
  Aws::ApplicationAutoScaling::Client.new(aws_options)
19
25
  end
@@ -14,6 +14,7 @@ class Ufo::Cfn::Stack::Builder::Resources
14
14
  def properties
15
15
  props = {
16
16
  Cluster: @cluster,
17
+ DeploymentConfiguration: deployment_configuration,
17
18
  DesiredCount: {
18
19
  "Fn::If": [
19
20
  "EcsDesiredCountIsBlank",
@@ -68,5 +69,17 @@ class Ufo::Cfn::Stack::Builder::Resources
68
69
 
69
70
  props
70
71
  end
72
+
73
+ private
74
+ def deployment_configuration
75
+ ecs = Ufo.config.ecs
76
+ return ecs.configuration if ecs.configuration # provide user full control
77
+
78
+ # default
79
+ {
80
+ MaximumPercent: ecs.maximum_percent,
81
+ MinimumHealthyPercent: ecs.minimum_healthy_percent,
82
+ }
83
+ end
71
84
  end
72
85
  end
@@ -32,8 +32,23 @@ class Ufo::Cfn::Stack::Builder::Resources
32
32
 
33
33
  props[:TargetType] = "ip" if vars[:container][:network_mode] == "awsvpc"
34
34
  props[:HealthCheckPort] = vars[:container][:port] if vars[:elb_type] == "network" && vars[:network_mode] == "awsvpc"
35
+ props[:HealthCheckPath] = health_check_path
36
+ props[:HealthCheckIntervalSeconds] = health_check_interval_seconds
37
+ props[:HealthyThresholdCount] = healthy_threshold_count
38
+ props[:UnhealthyThresholdCount] = unhealthy_threshold_count
35
39
 
36
40
  props
37
41
  end
42
+
43
+ meths = %w[
44
+ health_check_interval_seconds
45
+ health_check_path
46
+ healthy_threshold_count
47
+ unhealthy_threshold_count
48
+ ]
49
+ delegate *meths, to: :elb
50
+ def elb
51
+ Ufo.config.elb
52
+ end
38
53
  end
39
54
  end
@@ -24,6 +24,9 @@ class Ufo::Cfn::Stack
24
24
  path = ".ufo/output/template.yml"
25
25
  IO.write("#{Ufo.root}/#{path}", text)
26
26
  logger.info "Template built: #{path}"
27
+ # Only basic YAML validation. Doesnt check for everything CloudFormation checks.
28
+ # For CloudFormation checks handled with an exception handler in Cfn::Stack#print_code(exception)
29
+ Ufo::Yaml.validate!(path)
27
30
  text
28
31
  end
29
32
  end
data/lib/ufo/cfn/stack.rb CHANGED
@@ -52,16 +52,7 @@ module Ufo::Cfn
52
52
 
53
53
  def perform(action)
54
54
  logger.info "#{action[0..-2].capitalize}ing stack #{@stack_name.color(:green)}"
55
- # Example: cloudformation.send("update_stack", stack_options)
56
-
57
- # o = stack_options.dup
58
- # o[:template_body] = '...' if o[:template_body]
59
- # puts "stack_options:"
60
- # pp o
61
- # puts "parameters:"
62
- # pp o[:parameters]
63
-
64
- cloudformation.send("#{action}_stack", stack_options)
55
+ cloudformation.send("#{action}_stack", stack_options) # Example: cloudformation.send("update_stack", stack_options)
65
56
  rescue Aws::CloudFormation::Errors::ValidationError => e
66
57
  handle_stack_error(e)
67
58
  end
@@ -117,7 +108,6 @@ module Ufo::Cfn
117
108
  memoize :rollback_task_definition
118
109
 
119
110
  def exit_with_message(stack)
120
- region = `aws configure get region`.strip rescue "us-east-1"
121
111
  url = "https://console.aws.amazon.com/cloudformation/home?region=#{region}#/stacks"
122
112
  logger.info "The stack is not in an updateable state: #{stack.stack_status.color(:yellow)}."
123
113
  logger.info "Here's the CloudFormation url to check for more details #{url}"
@@ -134,13 +124,28 @@ module Ufo::Cfn
134
124
  if message.include?('UPDATE_ROLLBACK_FAILED')
135
125
  logger.info "You might be able to do a 'Continue Update Rollback' and skip some resources to get the stack back into a good state."
136
126
  end
137
- region = `aws configure get region`.strip rescue 'us-east-1'
138
127
  url = "https://console.aws.amazon.com/cloudformation/home?region=#{region}"
139
128
  logger.info "Here's the CloudFormation console url: #{url}"
140
129
  exit 1
141
130
  when /No updates are to be performed/
142
131
  logger.info "There are no updates to be performed. Exiting.".color(:yellow)
143
132
  exit 1
133
+ when /YAML not well-formed/ # happens if a value is a serialize Ruby Object. See: https://gist.github.com/tongueroo/737531d0bc8c92d92b5cd00493e15d9e
134
+ # e.message: Template format error: YAML not well-formed. (line 207, column 9)
135
+ print_code(e)
136
+ else
137
+ raise
138
+ end
139
+ end
140
+
141
+ def print_code(exception)
142
+ path = ".ufo/output/template.yml"
143
+ md = exception.message.match(/line (\d+),/)
144
+ line_number = md[1]
145
+ logger.error "Template for debugging: #{path}"
146
+ if md
147
+ DslEvaluator.print_code(path, line_number)
148
+ exit 1
144
149
  else
145
150
  raise
146
151
  end
@@ -171,5 +176,11 @@ module Ufo::Cfn
171
176
  logger.info "The stack is not in a state to that is cancelable: #{stack.stack_status}"
172
177
  end
173
178
  end
179
+
180
+ delegate :region, to: :aws
181
+ def aws
182
+ AwsData.new
183
+ end
184
+ memoize :aws
174
185
  end
175
186
  end
@@ -0,0 +1,12 @@
1
+ class Ufo::CLI::Central
2
+ class Base
3
+ include Ufo::Utils::Execute
4
+ include Ufo::Utils::Logging
5
+ include Ufo::Utils::Pretty
6
+ include Ufo::Utils::Sure
7
+
8
+ def initialize(options={})
9
+ @options = options
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ class Ufo::CLI::Central
2
+ class Clean < Base
3
+ def run
4
+ path = "#{ENV['HOME']}/.ufo/central"
5
+ sure?("Will remove folder with repo caches: #{pretty_home(path)}")
6
+ FileUtils.rm_rf(path)
7
+ logger.info "Removed: #{pretty_home(path)}"
8
+ end
9
+ end
10
+ end
@@ -1,68 +1,102 @@
1
1
  class Ufo::CLI::Central
2
- class Update
3
- include Ufo::Utils::Logging
4
- include Ufo::Utils::Execute
5
- include Ufo::Utils::Sure
6
-
7
- def initialize(options={})
8
- @options = options
9
- end
10
-
2
+ class Update < Base
11
3
  def run
12
- action = File.exist?(".ufo") ? "update" : "create"
13
- sure?("Will #{action} the .ufo folder.") # IE: Will create the .ufo folder.
14
4
  validate!
15
- logger.info "Updating .ufo with #{repo}"
5
+ action = File.exist?(".ufo") ? "update" : "create"
6
+ sure?("Will #{action} the .ufo symlink") # IE: Will create the .ufo symlink
7
+ logger.info "Updating .ufo with #{central_repo}"
16
8
  FileUtils.mkdir_p(tmp_area)
17
9
  pull
18
- sync
10
+ symlink
19
11
  check_gitignore
20
12
  end
21
13
 
22
14
  def pull
23
15
  logger.debug "Within #{tmp_area}"
24
16
  Dir.chdir(tmp_area) do
25
- if File.exist?(repo_name)
26
- execute "cd #{repo_name} && git pull"
17
+ if File.exist?(repo)
18
+ execute "cd #{repo} && git pull"
27
19
  else
28
- execute "git clone #{repo}"
20
+ execute "git clone #{central_repo}"
29
21
  end
30
22
  end
31
23
  end
32
24
 
33
- def sync
34
- FileUtils.mv(".ufo", ".ufo.bak") if File.exist?(".ufo")
35
- src = "#{tmp_area}/#{repo_name}"
36
- src += "/#{folder}" if folder
37
- FileUtils.cp_r(src, ".ufo")
25
+ # Always update the symlink in case use changes UFO_CENTRAL_REPO
26
+ def symlink
27
+ src = "#{tmp_area}/#{repo}"
28
+ src += "/#{central_folder}" if central_folder
29
+
30
+ FileUtils.mv(".ufo", ".ufo.bak") if File.exist?(".ufo") && File.directory?(".ufo")
31
+
32
+ # FileUtils.ln_s(target, link, options)
33
+ # ~/.ufo/central/repo -> .ufo
34
+ FileUtils.ln_sf(src, ".ufo", verbose: false) # force in case of existing broken symlink
38
35
  FileUtils.rm_rf(".ufo.bak")
39
- logger.info "The .ufo folder has been updated"
36
+
37
+ report_broken_symlink
38
+
39
+ logger.info "The .ufo symlink has been updated"
40
+ logger.info "Symlink: .ufo -> #{pretty_home(src)}"
41
+ end
42
+
43
+ def report_broken_symlink
44
+ return unless File.symlink?('.ufo')
45
+
46
+ message =<<~EOL.color(:red)
47
+ ERROR: The .ufo symlink appears to pointing to a missing folder.
48
+ Please double check that the folder exist in the repo/
49
+ EOL
50
+ begin
51
+ target = File.readlink('.ufo')
52
+ unless File.exist?(target)
53
+ logger.error message
54
+ logger.error "Symlink: .ufo -> #{target}"
55
+ exit 1
56
+ end
57
+ rescue Errno::EEXIST
58
+ logger.error message
59
+ exit 1
60
+ end
40
61
  end
41
62
 
42
63
  def validate!
43
- return if repo
64
+ return if central_repo
44
65
  logger.info "ERROR: Please set the env var: UFO_CENTRAL_REPO".color(:red)
66
+ logger.info "The ufo central update command requires it."
45
67
  exit 1
46
68
  end
47
69
 
48
- def repo_name
49
- File.basename(repo)
70
+ # Assume github.com:org/repo. May not work for private "ssh://host:repo" style repos
71
+ # See: https://terraspace.cloud/docs/terrafile/sources/ssh/
72
+ # Will consider PRs.
73
+ #
74
+ # org is used for path to ~/.ufo/central/org/repo
75
+ #
76
+ def org
77
+ base = central_repo.split('/')[-2] # 1. git@github.com:org/repo 2. repo (for case of https://github.com/org/repo)
78
+ base.gsub!(/.*:/,'') # git@github.com:org/repo => org/repo
79
+ base
50
80
  end
51
81
 
52
82
  def repo
83
+ File.basename(central_repo)
84
+ end
85
+
86
+ def central_repo
53
87
  ENV['UFO_CENTRAL_REPO']
54
88
  end
55
89
 
56
- def folder
90
+ def central_folder
57
91
  ENV['UFO_CENTRAL_FOLDER']
58
92
  end
59
93
 
60
94
  def tmp_area
61
- "/tmp/ufo/central"
95
+ "#{ENV['HOME']}/.ufo/central/#{org}"
62
96
  end
63
97
 
64
98
  def check_gitignore
65
- ok = true
99
+ ok = false
66
100
  if File.exist?('.gitignore')
67
101
  lines = IO.readlines('.gitignore')
68
102
  ok = lines.detect do |line|
@@ -8,5 +8,12 @@ class Ufo::CLI
8
8
  def update
9
9
  Update.new(options).run
10
10
  end
11
+
12
+ desc "clean", "remove ~/.ufo/central"
13
+ long_desc Help.text("central/clean")
14
+ opts.yes
15
+ def clean
16
+ Clean.new(options).run
17
+ end
11
18
  end
12
19
  end
data/lib/ufo/cli/clean.rb CHANGED
@@ -1,5 +1,3 @@
1
- require "fileutils"
2
-
3
1
  class Ufo::CLI
4
2
  class Clean < Base
5
3
  def run
@@ -18,7 +18,7 @@ class Ufo::CLI
18
18
  end
19
19
 
20
20
  cloudformation.delete_stack(stack_name: @stack_name)
21
- puts "Deleting stack with ECS resources: #{@stack_name}"
21
+ puts "Deleting stack #{@stack_name.color(:green)}"
22
22
 
23
23
  return unless @options[:wait]
24
24
  status.wait
@@ -17,10 +17,10 @@ Confirm to ship:
17
17
 
18
18
  Will deploy stack demo-web-dev (y/N) y
19
19
  Building Docker Image
20
- => docker build -t 536766270177.dkr.ecr.us-west-2.amazonaws.com/demo:ufo-2022-03-02T20-32-33-12dc6e0 -f Dockerfile .
21
- Docker Image built: 536766270177.dkr.ecr.us-west-2.amazonaws.com/demo:ufo-2022-03-02T20-32-33-12dc6e0
20
+ => docker build -t 111111111111.dkr.ecr.us-west-2.amazonaws.com/demo:ufo-2022-03-02T20-32-33-12dc6e0 -f Dockerfile .
21
+ Docker Image built: 111111111111.dkr.ecr.us-west-2.amazonaws.com/demo:ufo-2022-03-02T20-32-33-12dc6e0
22
22
  Pushing Docker Image
23
- => docker push 536766270177.dkr.ecr.us-west-2.amazonaws.com/demo:ufo-2022-03-02T20-32-33-12dc6e0
23
+ => docker push 111111111111.dkr.ecr.us-west-2.amazonaws.com/demo:ufo-2022-03-02T20-32-33-12dc6e0
24
24
  Task Definition built: .ufo/output/task_definition.json
25
25
  Parameters built: .ufo/output/params.json
26
26
  Template built: .ufo/output/template.yml
data/lib/ufo/cli/logs.rb CHANGED
@@ -16,7 +16,7 @@ class Ufo::CLI
16
16
 
17
17
  def find_log_group_name
18
18
  unless info.service
19
- logger.info "Cannot find ECS service for #{@stack_name}"
19
+ logger.info "Cannot find stack: #{@stack_name}"
20
20
  exit 1
21
21
  end
22
22
  task_definition = info.service.task_definition
@@ -0,0 +1,132 @@
1
+ class Ufo::CLI::Ps
2
+ class Errors < Ufo::CLI::Ps
3
+ extend Memoist
4
+
5
+ def initialize(options={})
6
+ super
7
+ @tasks = options[:tasks]
8
+ end
9
+
10
+ def show
11
+ message = recent_message
12
+ return unless message
13
+ return if message =~ /has reached a steady state/
14
+
15
+ scale
16
+ target_group
17
+ deployment_configuration
18
+ catchall
19
+ end
20
+
21
+ # If running count < desired account for a long time
22
+ # And see was unable to place a task
23
+ # Probably not enough capacity
24
+ def scale
25
+ return if service.running_count >= service.desired_count
26
+
27
+ error_event = recent_events.find do |e|
28
+ e.message =~ /was unable to place a task/
29
+ end
30
+ return unless error_event
31
+
32
+ logger.info "There is an issue scaling the #{@stack_name.color(:green)} service to #{service.desired_count}. Here's the error:"
33
+ logger.info error_event.message.color(:red)
34
+ if service.launch_type == "EC2"
35
+ logger.info <<~EOL
36
+ If AutoScaling is set up for the container instances,
37
+ it can take a little time to add additional instances.
38
+ You'll see this message until the capacity is added.
39
+ EOL
40
+ end
41
+ end
42
+
43
+ # The error currently happens to be the 5th element.
44
+ #
45
+ # Example:
46
+ # (service XXX) (instance i-XXX) (port 32875) is unhealthy in (target-group arn:aws:elasticloadbalancing:us-east-1:111111111111:targetgroup/devel-Targe-1111111111111/1111111111111111) due to (reason Health checks failed with these codes: [400])">]
47
+ def target_group
48
+ error_event = recent_events.find do |e|
49
+ e.message =~ /is unhealthy in/ &&
50
+ e.message =~ /targetgroup/
51
+ end
52
+ return unless error_event
53
+
54
+ logger.error "There are targets in the target group reporting unhealthy. This can cause containers to cycle. Here's the error:"
55
+ logger.error error_event.message.color(:red)
56
+ logger.error <<~EOL
57
+ Check out the ECS console and EC2 Load Balancer console for more info.
58
+ Sometimes they may not helpful :(
59
+ Docs that may help: https://ufoships.com/docs/debug/unhealthy-targets/
60
+ EOL
61
+ end
62
+
63
+ # To reproduce
64
+ #
65
+ # .ufo/config.rb
66
+ #
67
+ # Ufo.configure do |config|
68
+ # config.ecs.maximum_percent = 150 # need at least 200 to go from 1 to 2 containers
69
+ # config.ecs.minimum_healthy_percent = 100
70
+ # end
71
+ #
72
+ # Event message error:
73
+ #
74
+ # ERROR: (service app1-web-dev-EcsService-8FMliG8m6M2p) was unable to stop or start tasks during a deployment because of the service deployment configuration. Update the minimumHealthyPercent or maximumPercent value and try again.
75
+ #
76
+ def deployment_configuration
77
+ message = recent_message
78
+ return unless message.include?("unable") && message.include?("deployment configuration")
79
+
80
+ logger.error "ERROR: Deployment Configuration".color(:red)
81
+ logger.error <<~EOL
82
+ You might have a Deployment Configuration that prevents the deployment from completing.
83
+
84
+ See: https://ufoships.com/docs/debug/deployment-configuration/
85
+
86
+ EOL
87
+ end
88
+
89
+ # Example:
90
+ # (service app1-web-dev-EcsService-8FMliG8m6M2p) was unable to stop or start tasks during a deployment because of the service deployment configuration. Update the minimumHealthyPercent or maximumPercent value and try again.
91
+ def catchall
92
+ words = %w[fail unable error]
93
+ recent_messages = recent_events.map(&:message)
94
+ message = recent_messages.find do |message|
95
+ words.detect { |word| message.include?(word) }
96
+ end
97
+
98
+ return unless message
99
+ logger.error "ERROR: #{message}".color(:red)
100
+
101
+ logger.error <<~EOL
102
+ You might have to #{cancel_actions[:cfn]} the stack with:
103
+
104
+ ufo #{cancel_actions[:ufo]}
105
+
106
+ And try again after fixing the issue.
107
+ EOL
108
+ end
109
+
110
+ private
111
+ def cancel_actions
112
+ stack = find_stack(@stack_name)
113
+ if stack && stack.stack_status == "CREATE_COMPLETE"
114
+ { cfn: "delete", ufo: "destroy" }
115
+ else
116
+ { cfn: "cancel", ufo: "cancel" }
117
+ end
118
+ end
119
+ memoize :cancel_actions
120
+
121
+ # only check a few most recent
122
+ def recent_events
123
+ service["events"][0..4]
124
+ end
125
+
126
+ def recent_message
127
+ recent = recent_events.first
128
+ return unless recent
129
+ recent.message ? recent.message : nil
130
+ end
131
+ end
132
+ end
@@ -1,11 +1,8 @@
1
1
  class Ufo::CLI::Ps
2
- class Task
3
- def self.header
4
- %w[Task Name Release Started Status Notes]
5
- end
6
-
7
- def initialize(task)
8
- @task = task
2
+ class Task < Ufo::CLI::Base
3
+ def initialize(options={})
4
+ super
5
+ @task = options[:task] # task response from ecs.list_tasks
9
6
  end
10
7
 
11
8
  def to_a
@@ -52,9 +49,11 @@ class Ufo::CLI::Ps
52
49
  # stopping_at=2018-07-05 22:03:44 -0700,
53
50
  # stopped_at=2018-07-05 22:03:45 -0700,
54
51
  def hide?
55
- stopped_at = time(@task["stopped_at"])
52
+ return false if @options[:status] == "stopped"
53
+ started_at = time(@task["started_at"])
54
+ return false unless started_at # edge case when started_at not yet set
56
55
  time = Time.now - 60 * Ufo.config.ps.hide_age
57
- status == "STOPPED" && stopped_at < time
56
+ status == "STOPPED" && started_at < time
58
57
  end
59
58
 
60
59
  def status
@@ -92,5 +91,11 @@ class Ufo::CLI::Ps
92
91
  start_time.strftime("%m/%d/%Y")
93
92
  end
94
93
  end
94
+
95
+ class << self
96
+ def header
97
+ %w[Task Name Release Started Status Notes]
98
+ end
99
+ end
95
100
  end
96
101
  end
data/lib/ufo/cli/ps.rb CHANGED
@@ -9,7 +9,12 @@ class Ufo::CLI
9
9
 
10
10
  def run
11
11
  unless service
12
- logger.info info.no_service_message
12
+ stack = find_stack(@stack_name)
13
+ if stack && stack.stack_status == "CREATE_IN_PROGRESS"
14
+ logger.info "Stack is still creating. Try again after it completes."
15
+ else
16
+ logger.info info.no_service_message
17
+ end
13
18
  return
14
19
  end
15
20
 
@@ -25,9 +30,8 @@ class Ufo::CLI
25
30
  resp["tasks"]
26
31
  end.flatten
27
32
 
28
- display_tasks(all_task_arns)
29
- display_scale_help
30
- display_target_group_help
33
+ tasks = show_tasks(all_task_arns)
34
+ show_errors(tasks)
31
35
  end
32
36
 
33
37
  def summary
@@ -63,50 +67,14 @@ class Ufo::CLI
63
67
  resp.scalable_targets.first # scalable_target
64
68
  end
65
69
 
66
- def display_target_group_help
67
- events = service["events"][0..4]
68
- return if events[0].message =~ /has reached a steady state/
69
-
70
- # The error currently happens to be the 5th element.
71
- #
72
- # Example:
73
- # "(service XXX) (instance i-XXX) (port 32875) is unhealthy in (target-group arn:aws:elasticloadbalancing:us-east-1:111111111111:targetgroup/devel-Targe-1111111111111/1111111111111111) due to (reason Health checks failed with these codes: [400])">]
74
- error_event = events.find do |e|
75
- e.message =~ /is unhealthy in/ &&
76
- e.message =~ /targetgroup/
77
- end
78
- return unless error_event
79
-
80
- logger.error "There are targets in the target group reporting unhealthy. This can cause containers to cycle. Here's the error:"
81
- logger.error error_event.message.color(:red)
82
- logger.error <<~EOL
83
- Check out the ECS console and EC2 Load Balancer console for more info.
84
- Sometimes they may not helpful :(
85
- Docs that may help: https://ufoships.com/docs/debug/unhealthy-targets/
86
- EOL
87
- end
88
-
89
- # If the running count less than the desired account yet, check the events
90
- # and show a message with helpful debugging information.
91
- def display_scale_help
92
- return if service.running_count >= service.desired_count
93
-
94
- events = service["events"][0..3] # only check most recent 4 messages
95
- error_event = events.find do |e|
96
- e.message =~ /was unable to place a task/
97
- end
98
- return unless error_event
99
-
100
- logger.info "There is an issue scaling the #{@service.color(:green)} service to #{service.desired_count}. Here's the error:"
101
- logger.info error_event.message.color(:red)
102
- if service.launch_type == "EC2"
103
- logger.info "If AutoScaling is set up for the container instances, it can take a little time to add additional instances. You'll see this message until the capacity is added."
104
- end
70
+ def convert_to_task_objects(task_arns)
71
+ task_arns.sort_by! { |t| t["task_arn"] }
72
+ task_arns.map { |t| Task.new(@options.merge(task: t)) } # will have Task objects after this point
105
73
  end
106
74
 
107
- def display_tasks(raw_tasks)
108
- raw_tasks.sort_by! { |t| t["task_arn"] }
109
- tasks = raw_tasks.map { |t| Task.new(t) } # will have Task objects after this point
75
+ # Note: ufo stop also uses Ps#show_tasks. Thats why convert_to_task_objects within the method
76
+ def show_tasks(tasks_arns)
77
+ tasks = convert_to_task_objects(tasks_arns)
110
78
  tasks = tasks.reject(&:hide?)
111
79
  show_notes = show_notes(tasks)
112
80
 
@@ -122,8 +90,14 @@ class Ufo::CLI
122
90
  presenter.rows << row
123
91
  end
124
92
  presenter.show
93
+ tasks
94
+ end
95
+
96
+ def show_errors(tasks)
97
+ Errors.new(@options.merge(tasks: tasks)).show
125
98
  end
126
99
 
100
+ private
127
101
  def show_notes(tasks)
128
102
  tasks.detect { |t| !t.notes.blank? }
129
103
  end
data/lib/ufo/cli/scale.rb CHANGED
@@ -45,6 +45,11 @@ class Ufo::CLI
45
45
  scalable_target = stack_resources.find do |r|
46
46
  r.logical_resource_id == "ScalingTarget"
47
47
  end
48
+ register_scalable_target(scalable_target)
49
+ logger.info "Configured autoscaling to min: #{@min} max: #{@max}"
50
+ end
51
+
52
+ def register_scalable_target(scalable_target)
48
53
  # service/dev/app1-web-dev-EcsService-Q0XkN6VtxGWv|ecs:service:DesiredCount|ecs
49
54
  resource_id, scalable_dimension, service_namespace = scalable_target.physical_resource_id.split('|')
50
55
  applicationautoscaling.register_scalable_target(
@@ -54,7 +59,9 @@ class Ufo::CLI
54
59
  scalable_dimension: scalable_dimension,
55
60
  service_namespace: service_namespace,
56
61
  )
57
- logger.info "Configured autoscaling to min: #{@min} max: #{@max}"
62
+ rescue Aws::ApplicationAutoScaling::Errors::ValidationException => e
63
+ logger.error "ERROR: #{e.class} #{e.message}".color(:red)
64
+ exit 1
58
65
  end
59
66
 
60
67
  def warning
@@ -68,7 +75,7 @@ class Ufo::CLI
68
75
 
69
76
  You can also turn off this warning with
70
77
 
71
- config.scaling.warning = false
78
+ config.scale.warning = false
72
79
 
73
80
  EOL
74
81
  end
data/lib/ufo/cli/stop.rb CHANGED
@@ -29,7 +29,7 @@ class Ufo::CLI
29
29
  def show(tasks, preview: true)
30
30
  logger.info "Will stop the following tasks:" if preview
31
31
  ps = Ps.new(@options)
32
- ps.display_tasks(tasks)
32
+ ps.show_tasks(tasks)
33
33
  end
34
34
 
35
35
  # latest deployment task definition arn
data/lib/ufo/cli.rb CHANGED
@@ -85,7 +85,8 @@ module Ufo
85
85
  long_desc Help.text(:ps)
86
86
  option :status, default: "all", desc: "Status filter: all, pending, stopped, running."
87
87
  # not setting format default so we can use Ufo.config.ps.format and dont want to trigger a config load this early
88
- option :format, desc: "Output formats: #{CliFormat.formats.join(', ')}"
88
+ formats = CliFormat.formats + ["auto"]
89
+ option :format, desc: "Output formats: #{formats.sort.join(', ')}"
89
90
  def ps
90
91
  Ps.new(options).run
91
92
  end
data/lib/ufo/config.rb CHANGED
@@ -49,24 +49,33 @@ module Ufo
49
49
 
50
50
  config.ecs = ActiveSupport::OrderedOptions.new
51
51
  config.ecs.cluster = ":ENV" # => dev
52
+ config.ecs.deployment_configuration = nil # full control
52
53
  config.ecs.desired_count = nil # only respected when config.autoscaling.enabled = false
54
+ config.ecs.maximum_percent = 200 # nil
55
+ config.ecs.minimum_healthy_percent = 100 # nil
53
56
  config.ecs.scheduling_strategy = "REPLICA"
54
57
 
55
58
  config.elb = ActiveSupport::OrderedOptions.new
56
- config.elb.subnet_mappings = nil # static IP addresses for network load balancer
59
+ config.elb.default_actions = nil # full override
57
60
  config.elb.enabled = "auto" # "auto", true or false
61
+
62
+ config.elb.health_check_interval_seconds = 10
63
+ config.elb.health_check_path = nil # When nil its AWS default /
64
+ config.elb.healthy_threshold_count = 5
65
+ config.elb.unhealthy_threshold_count = 2
66
+
58
67
  config.elb.port = 80 # default listener port
59
- config.elb.ssl = ActiveSupport::OrderedOptions.new
60
- config.elb.ssl.certificates = nil
61
- config.elb.ssl.enabled = false
62
- config.elb.ssl.port = 443
63
- config.elb.type = "application"
64
- config.elb.default_actions = nil # full override
65
68
  config.elb.redirect = ActiveSupport::OrderedOptions.new
66
69
  config.elb.redirect.code = 302 # IE: 302 or 301
67
70
  config.elb.redirect.enabled = false
68
71
  config.elb.redirect.port = 443
69
72
  config.elb.redirect.protocol = "HTTPS"
73
+ config.elb.ssl = ActiveSupport::OrderedOptions.new
74
+ config.elb.ssl.certificates = nil
75
+ config.elb.ssl.enabled = false
76
+ config.elb.ssl.port = 443
77
+ config.elb.subnet_mappings = nil # static IP addresses for network load balancer
78
+ config.elb.type = "application"
70
79
 
71
80
  config.exec = ActiveSupport::OrderedOptions.new
72
81
  config.exec.command = "/bin/bash" # aws ecs execute-command cli
data/lib/ufo/info.rb CHANGED
@@ -40,6 +40,7 @@ module Ufo
40
40
  service = stack_resources.find { |r| r.resource_type == "AWS::ECS::Service" }
41
41
  return unless service # stack is still creating
42
42
  arn = service.physical_resource_id
43
+ return unless arn # can be nil for a few seconds while stack is still creating it
43
44
  resp = ecs.describe_services(services: [arn], cluster: @cluster)
44
45
  resp.services.first
45
46
  end
@@ -4,15 +4,22 @@ class Ufo::TaskDefinition::Erb
4
4
  text = IO.read(@path)
5
5
  YAML.load(text)
6
6
  rescue Psych::SyntaxError => e
7
- logger.error "ERROR: #{e.class}: #{e.message}"
7
+ logger.error "ERROR: #{e.class}: #{e.message}".color(:red)
8
8
  logger.error <<~EOL
9
9
  Rendered file contains invalid YAML. For debugging, files available at:
10
10
 
11
11
  source: #{@task_definition.path}
12
- rendered: #{@path}
12
+ compiled: #{@path}
13
13
 
14
14
  EOL
15
- print_code(text)
15
+
16
+ md = e.message.match(/at line (\d+) column (\d+)/)
17
+ if md
18
+ line_number = md[1]
19
+ DslEvaluator.print_code(@path, line_number)
20
+ else
21
+ print_code(text) # fallback to simpler print code if cannot find line numbers
22
+ end
16
23
  end
17
24
  end
18
25
  end
@@ -21,6 +21,11 @@ class Ufo::TaskDefinition
21
21
  end
22
22
 
23
23
  def check_empty!(data)
24
+ if data.nil?
25
+ logger.error "ERROR: Unable to compile the YAML".color(:red) # invalid YAML will result in data == nil
26
+ exit 1
27
+ end
28
+
24
29
  return unless data == true || data == false || data.empty?
25
30
  logger.error "ERROR: Empty task definition results".color(:red)
26
31
  logger.error <<~EOL
@@ -0,0 +1,18 @@
1
+ module Ufo::TaskDefinition::Helpers
2
+ module Acm
3
+ # returns cert arn
4
+ def acm_cert(domain)
5
+ certs = acm_certs
6
+ cert = certs.find do |c|
7
+ c.domain_name == domain
8
+ end
9
+ cert.certificate_arn if cert
10
+ end
11
+
12
+ # TODO: handle when there are lots of certs by paging
13
+ def acm_certs
14
+ resp = acm.list_certificates
15
+ resp.certificate_summary_list
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ module Ufo::TaskDefinition::Helpers
2
+ module Ecr
3
+ def ecr_repo(name)
4
+ repository = ecr_repository(name)
5
+ repository.repository_uri
6
+ end
7
+
8
+ def ecr_repository(name)
9
+ resp = ecr.describe_repositories(repository_names: [name])
10
+ resp.repositories.first
11
+ end
12
+ end
13
+ end
@@ -65,8 +65,8 @@ module Ufo::TaskDefinition::Helpers
65
65
  end
66
66
  end
67
67
 
68
- # arn:aws:ssm:us-west-2:536766270177:parameter/demo/dev/DB-NAME
69
- # arn:aws:ssm:us-west-2:536766270177:parameter/demo/dev/DB-NAME
68
+ # arn:aws:ssm:us-west-2:111111111111:parameter/demo/dev/DB-NAME
69
+ # arn:aws:ssm:us-west-2:111111111111:parameter/demo/dev/DB-NAME
70
70
  def expansion(arn)
71
71
  # performance improvement only run names.expansion on the name portion
72
72
  md = arn.match(/(.*:)(parameter\/|secret:)(.*)/)
@@ -4,6 +4,11 @@ module Ufo::Utils
4
4
  path.sub("#{Ufo.root}/",'').sub(/^\.\//,'')
5
5
  end
6
6
 
7
+ # Replace HOME with ~ - different from the main pretty_path
8
+ def pretty_home(path)
9
+ path.sub(ENV['HOME'], '~')
10
+ end
11
+
7
12
  # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby
8
13
  def pretty_time(total_seconds)
9
14
  minutes = (total_seconds / 60) % 60
data/lib/ufo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ufo
2
- VERSION = "6.0.0"
2
+ VERSION = "6.0.3"
3
3
  end
@@ -1,5 +1,7 @@
1
1
  class Ufo::Yaml
2
2
  class Validator
3
+ include Ufo::Utils::Logging
4
+
3
5
  def initialize(path)
4
6
  @path = path
5
7
  end
@@ -18,34 +20,16 @@ class Ufo::Yaml
18
20
  end
19
21
 
20
22
  def handle_yaml_syntax_error(e, path)
21
- io = StringIO.new
22
- io.puts "Invalid yaml. Output written for debugging: #{path}".color(:red)
23
- io.puts "ERROR: #{e.message}".color(:red)
23
+ logger.error "ERROR: #{e.message}".color(:red)
24
+ logger.error "Invalid yaml. Output written for debugging: #{path}".color(:red)
24
25
 
25
26
  # Grab line info. Example error:
26
27
  # ERROR: (<unknown>): could not find expected ':' while scanning a simple key at line 2 column 1
27
28
  md = e.message.match(/at line (\d+) column (\d+)/)
28
29
  line = md[1].to_i
29
30
 
30
- lines = IO.read(path).split("\n")
31
- context = 5 # lines of context
32
- top, bottom = [line-context-1, 0].max, line+context-1
33
- spacing = lines.size.to_s.size
34
- lines[top..bottom].each_with_index do |line_content, index|
35
- line_number = top+index+1
36
- if line_number == line
37
- io.printf("%#{spacing}d %s\n".color(:red), line_number, line_content)
38
- else
39
- io.printf("%#{spacing}d %s\n", line_number, line_content)
40
- end
41
- end
42
-
43
- if ENV['LONO_TEST']
44
- io.string
45
- else
46
- puts io.string
47
- exit 1
48
- end
31
+ DslEvaluator.print_code(path, line_number)
32
+ exit 1
49
33
  end
50
34
  end
51
35
  end
data/lib/ufo/yaml.rb CHANGED
@@ -8,6 +8,10 @@ module Ufo
8
8
  Validator.new(path).validate!
9
9
  Loader.new(text).load
10
10
  end
11
+
12
+ def validate!(path)
13
+ Validator.new(path).validate!
14
+ end
11
15
  end
12
16
  end
13
17
  end
data/ufo.gemspec CHANGED
@@ -20,19 +20,20 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency "aws-logs"
22
22
  spec.add_dependency "aws-mfa-secure", ">= 0.4.3"
23
+ spec.add_dependency "aws-sdk-acm"
23
24
  spec.add_dependency "aws-sdk-applicationautoscaling"
24
25
  spec.add_dependency "aws-sdk-cloudformation"
25
26
  spec.add_dependency "aws-sdk-cloudwatchlogs"
26
27
  spec.add_dependency "aws-sdk-ec2"
27
28
  spec.add_dependency "aws-sdk-ecr"
28
29
  spec.add_dependency "aws-sdk-ecs"
29
- spec.add_dependency "aws-sdk-ssm"
30
30
  spec.add_dependency "aws-sdk-elasticloadbalancingv2"
31
+ spec.add_dependency "aws-sdk-ssm"
31
32
  spec.add_dependency "aws_data"
32
33
  spec.add_dependency "cfn-status"
33
34
  spec.add_dependency "cli-format"
34
35
  spec.add_dependency "deep_merge"
35
- spec.add_dependency "dsl_evaluator"
36
+ spec.add_dependency "dsl_evaluator", ">= 0.2.5" # for DslEvaluator.print_code
36
37
  spec.add_dependency "memoist"
37
38
  spec.add_dependency "plissken"
38
39
  spec.add_dependency "rainbow"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ufo
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0
4
+ version: 6.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-05 00:00:00.000000000 Z
11
+ date: 2022-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-logs
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.4.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-acm
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: aws-sdk-applicationautoscaling
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -123,7 +137,7 @@ dependencies:
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
- name: aws-sdk-ssm
140
+ name: aws-sdk-elasticloadbalancingv2
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - ">="
@@ -137,7 +151,7 @@ dependencies:
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
- name: aws-sdk-elasticloadbalancingv2
154
+ name: aws-sdk-ssm
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - ">="
@@ -212,14 +226,14 @@ dependencies:
212
226
  requirements:
213
227
  - - ">="
214
228
  - !ruby/object:Gem::Version
215
- version: '0'
229
+ version: 0.2.5
216
230
  type: :runtime
217
231
  prerelease: false
218
232
  version_requirements: !ruby/object:Gem::Requirement
219
233
  requirements:
220
234
  - - ">="
221
235
  - !ruby/object:Gem::Version
222
- version: '0'
236
+ version: 0.2.5
223
237
  - !ruby/object:Gem::Dependency
224
238
  name: memoist
225
239
  requirement: !ruby/object:Gem::Requirement
@@ -484,6 +498,8 @@ files:
484
498
  - lib/ufo/cli/build.rb
485
499
  - lib/ufo/cli/cancel.rb
486
500
  - lib/ufo/cli/central.rb
501
+ - lib/ufo/cli/central/base.rb
502
+ - lib/ufo/cli/central/clean.rb
487
503
  - lib/ufo/cli/central/update.rb
488
504
  - lib/ufo/cli/clean.rb
489
505
  - lib/ufo/cli/destroy.rb
@@ -518,6 +534,7 @@ files:
518
534
  - lib/ufo/cli/new/sequence.rb
519
535
  - lib/ufo/cli/opts.rb
520
536
  - lib/ufo/cli/ps.rb
537
+ - lib/ufo/cli/ps/errors.rb
521
538
  - lib/ufo/cli/ps/task.rb
522
539
  - lib/ufo/cli/releases.rb
523
540
  - lib/ufo/cli/rollback.rb
@@ -569,8 +586,10 @@ files:
569
586
  - lib/ufo/task_definition/erb/json.rb
570
587
  - lib/ufo/task_definition/erb/yaml.rb
571
588
  - lib/ufo/task_definition/helpers.rb
589
+ - lib/ufo/task_definition/helpers/acm.rb
572
590
  - lib/ufo/task_definition/helpers/aws_data_helper.rb
573
591
  - lib/ufo/task_definition/helpers/core.rb
592
+ - lib/ufo/task_definition/helpers/ecr.rb
574
593
  - lib/ufo/task_definition/helpers/ssm.rb
575
594
  - lib/ufo/task_definition/helpers/ssm/fetcher.rb
576
595
  - lib/ufo/task_definition/helpers/stack_output.rb