ufo 4.6.3 → 5.0.0

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/docs/_docs/extras/notification-arns.md +21 -0
  4. data/docs/_docs/helpers.md +6 -4
  5. data/docs/_docs/iam-roles.md +111 -0
  6. data/docs/_docs/secrets.md +112 -0
  7. data/docs/_docs/settings/cluster.md +7 -13
  8. data/docs/_includes/subnav.html +3 -0
  9. data/docs/_reference/ufo-deploy.md +1 -2
  10. data/docs/_reference/ufo-logs.md +1 -1
  11. data/docs/_reference/ufo-rollback.md +2 -0
  12. data/docs/_reference/ufo-ship.md +1 -2
  13. data/docs/_reference/ufo-ships.md +1 -2
  14. data/docs/_reference/ufo-tasks-build.md +1 -2
  15. data/lib/template/.secrets +3 -0
  16. data/lib/template/.ufo/settings.yml.tt +1 -0
  17. data/lib/template/.ufo/settings/cfn/default.yml.tt +27 -27
  18. data/lib/template/.ufo/settings/network/default.yml.tt +9 -0
  19. data/lib/template/.ufo/templates/fargate.json.erb +3 -0
  20. data/lib/template/.ufo/templates/main.json.erb +3 -0
  21. data/lib/template/.ufo/variables/base.rb.tt +1 -0
  22. data/lib/ufo.rb +2 -1
  23. data/lib/ufo/autoloader.rb +9 -0
  24. data/lib/ufo/cli.rb +3 -2
  25. data/lib/ufo/core.rb +1 -9
  26. data/lib/ufo/docker/cleaner.rb +1 -1
  27. data/lib/ufo/dsl.rb +6 -1
  28. data/lib/ufo/dsl/helper.rb +19 -37
  29. data/lib/ufo/dsl/helper/vars.rb +98 -0
  30. data/lib/ufo/dsl/outputter.rb +12 -9
  31. data/lib/ufo/log_group.rb +1 -0
  32. data/lib/ufo/role/builder.rb +66 -0
  33. data/lib/ufo/role/dsl.rb +21 -0
  34. data/lib/ufo/role/registry.rb +24 -0
  35. data/lib/ufo/rollback.rb +2 -1
  36. data/lib/ufo/setting/profile.rb +11 -7
  37. data/lib/ufo/setting/security_groups.rb +22 -0
  38. data/lib/ufo/settings.rb +20 -0
  39. data/lib/ufo/stack.rb +24 -24
  40. data/lib/ufo/stack/builder.rb +26 -0
  41. data/lib/ufo/stack/builder/base.rb +54 -0
  42. data/lib/ufo/stack/builder/conditions.rb +23 -0
  43. data/lib/ufo/stack/builder/outputs.rb +24 -0
  44. data/lib/ufo/stack/builder/parameters.rb +45 -0
  45. data/lib/ufo/stack/builder/resources.rb +20 -0
  46. data/lib/ufo/stack/builder/resources/base.rb +4 -0
  47. data/lib/ufo/stack/builder/resources/dns.rb +17 -0
  48. data/lib/ufo/stack/builder/resources/ecs.rb +63 -0
  49. data/lib/ufo/stack/builder/resources/elb.rb +45 -0
  50. data/lib/ufo/stack/builder/resources/listener.rb +42 -0
  51. data/lib/ufo/stack/builder/resources/listener_ssl.rb +16 -0
  52. data/lib/ufo/stack/builder/resources/roles/base.rb +22 -0
  53. data/lib/ufo/stack/builder/resources/roles/execution_role.rb +4 -0
  54. data/lib/ufo/stack/builder/resources/roles/task_role.rb +4 -0
  55. data/lib/ufo/stack/builder/resources/security_group/base.rb +4 -0
  56. data/lib/ufo/stack/builder/resources/security_group/ecs.rb +44 -0
  57. data/lib/ufo/stack/builder/resources/security_group/ecs_rule.rb +25 -0
  58. data/lib/ufo/stack/builder/resources/security_group/elb.rb +57 -0
  59. data/lib/ufo/stack/builder/resources/target_group.rb +39 -0
  60. data/lib/ufo/stack/builder/resources/task_definition.rb +24 -0
  61. data/lib/ufo/stack/builder/resources/task_definition/reconstructor.rb +49 -0
  62. data/lib/ufo/stack/context.rb +41 -48
  63. data/lib/ufo/stack/custom_properties.rb +59 -0
  64. data/lib/ufo/stack/helper.rb +2 -5
  65. data/lib/ufo/stack/template_body.rb +13 -0
  66. data/lib/ufo/task.rb +2 -7
  67. data/lib/ufo/tasks.rb +1 -1
  68. data/lib/ufo/tasks/builder.rb +0 -1
  69. data/lib/ufo/template_scope.rb +1 -66
  70. data/lib/ufo/utils/squeezer.rb +24 -0
  71. data/lib/ufo/version.rb +1 -1
  72. data/spec/fixtures/iam_roles/task_role.rb +17 -0
  73. data/spec/lib/role/builder_spec.rb +67 -0
  74. data/spec/lib/role/dsl_spec.rb +12 -0
  75. data/ufo.gemspec +1 -0
  76. metadata +57 -3
  77. data/lib/cfn/stack.yml +0 -283
@@ -15,3 +15,12 @@ elb_subnets: # defaults to same subnets as ecs_subnets when not set
15
15
  # ecs_security_groups:
16
16
  # - sg-bbb
17
17
  # - sg-ccc
18
+
19
+ # Also supports extra security groups specific to each ECS service
20
+ # ecs_security_groups:
21
+ # demo-web:
22
+ # - sg-bbb
23
+ # - sg-ccc
24
+ # demo-worker:
25
+ # - sg-bbb
26
+ # - sg-ccc
@@ -21,6 +21,9 @@
21
21
  <% if @environment %>
22
22
  "environment": <%= @environment.to_json %>,
23
23
  <% end %>
24
+ <% if @secrets %>
25
+ "secrets": <%= @secrets.to_json %>,
26
+ <% end %>
24
27
  <% if @awslogs_group %>
25
28
  "logConfiguration": {
26
29
  "logDriver": "awslogs",
@@ -24,6 +24,9 @@
24
24
  <% if @environment %>
25
25
  "environment": <%= @environment.to_json %>,
26
26
  <% end %>
27
+ <% if @secrets %>
28
+ "secrets": <%= @secrets.to_json %>,
29
+ <% end %>
27
30
  <% if @awslogs_group %>
28
31
  "logConfiguration": {
29
32
  "logDriver": "awslogs",
@@ -2,6 +2,7 @@
2
2
  # More info on how variables work: http://ufoships.com/docs/variables/
3
3
  @image = helper.full_image_name # includes the git sha tongueroo/demo-ufo:ufo-[sha].
4
4
  @environment = helper.env_file(".env")
5
+ @secrets = helper.secrets_file(".secrets")
5
6
  <% if @options[:launch_type] == "fargate" -%>
6
7
  # Ensure that the cpu and memory values are a supported combination by Fargate.
7
8
  # More info: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html"
data/lib/ufo.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  $stdout.sync = true unless ENV["UFO_STDOUT_SYNC"] == "0"
2
2
 
3
3
  $:.unshift(File.expand_path('../', __FILE__))
4
- require 'deep_merge'
4
+ require 'active_support/core_ext/class'
5
+ require 'deep_merge/rails_compat'
5
6
  require 'fileutils'
6
7
  require 'memoist'
7
8
  require 'rainbow/ext/string'
@@ -14,8 +14,17 @@ module Ufo
14
14
  loader = Zeitwerk::Loader.new
15
15
  loader.inflector = Inflector.new
16
16
  loader.push_dir(File.dirname(__dir__)) # lib
17
+
18
+ helpers = "#{ufo_root}/.ufo/helpers"
19
+ loader.push_dir(helpers) if File.exist?(helpers) # project helpers
20
+
17
21
  loader.setup
18
22
  end
23
+
24
+ # Autoloader runs so early that Ufo.root is not available, so we must declare it here
25
+ def ufo_root
26
+ ENV['UFO_ROOT'] || '.'
27
+ end
19
28
  end
20
29
  end
21
30
  end
@@ -37,11 +37,11 @@ module Ufo
37
37
  option :elb, desc: "Decides to create elb, not create elb or use existing target group."
38
38
  option :elb_eip_ids, type: :array, desc: "EIP Allocation ids to use for network load balancer."
39
39
  option :elb_type, desc: "ELB type: application or network. Keep current deployed elb type when not specified."
40
- option :pretty, type: :boolean, default: true, desc: "Pretty format the json for the task definitions"
41
40
  option :scheduling_strategy, desc: "Scheduling strategy to use for the service. IE: replica, daemon"
42
41
  option :stop_old_tasks, type: :boolean, default: false, desc: "Stop old tasks as part of deployment to speed it up"
43
42
  option :task, desc: "ECS task name, to override the task name convention."
44
43
  option :wait, type: :boolean, desc: "Wait for deployment to complete", default: true
44
+ option :image_override, desc: "Override image in task definition for quick testing"
45
45
  end
46
46
 
47
47
  desc "deploy SERVICE", "Deploy task definition to ECS service without re-building the definition."
@@ -75,6 +75,7 @@ module Ufo
75
75
 
76
76
  desc "rollback SERVICE VERSION", "Rolls back to older task definition."
77
77
  long_desc Help.text(:rollback)
78
+ option :wait, type: :boolean, desc: "Wait for deployment to complete", default: true
78
79
  def rollback(service=:current, version)
79
80
  service = service == :current ? Current.service! : service
80
81
  rollback = Rollback.new(service, options.merge(version: version))
@@ -191,7 +192,7 @@ module Ufo
191
192
  long_desc Help.text(:logs)
192
193
  option :follow, default: true, type: :boolean, desc: " Whether to continuously poll for new logs. To exit from this mode, use Control-C."
193
194
  option :since, desc: "From what time to begin displaying logs. By default, logs will be displayed starting from 1 minutes in the past. The value provided can be an ISO 8601 timestamp or a relative time."
194
- option :format, default: "simple", desc: "The format to display the logs. IE: detailed or short. With detailed, the log stream name is also shown."
195
+ option :format, default: "detailed", desc: "The format to display the logs. IE: detailed or short. With detailed, the log stream name is also shown."
195
196
  option :filter_pattern, desc: "The filter pattern to use. If not provided, all the events are matched"
196
197
  def logs(service=:current)
197
198
  Logs.new(service, options).run
@@ -4,6 +4,7 @@ require 'yaml'
4
4
  module Ufo
5
5
  module Core
6
6
  extend Memoist
7
+ include Ufo::Settings
7
8
 
8
9
  def check_task_definition!(task_definition)
9
10
  task_definition_path = "#{Ufo.root}/.ufo/output/#{task_definition}.json"
@@ -49,15 +50,6 @@ module Ufo
49
50
  end
50
51
  end
51
52
 
52
- def settings
53
- Setting.new.data
54
- end
55
- memoize :settings
56
-
57
- def cfn_profile
58
- settings[:cfn_profile] || "default"
59
- end
60
-
61
53
  def check_ufo_project!
62
54
  check_path = "#{Ufo.root}/.ufo/settings.yml"
63
55
  unless File.exist?(check_path)
@@ -21,7 +21,7 @@ module Ufo
21
21
  end
22
22
 
23
23
  def delete_list
24
- return [] if ENV['TEST']
24
+ return [] if ENV['TEST'] || @options[:noop]
25
25
  return @delete_list if @delete_list
26
26
 
27
27
  out = execute("docker images") # live to override the noop cli options
@@ -2,6 +2,8 @@ require 'ostruct'
2
2
 
3
3
  module Ufo
4
4
  class DSL
5
+ extend Memoist
6
+
5
7
  def initialize(template_definitions_path, options={})
6
8
  @template_definitions_path = template_definitions_path
7
9
  @options = options
@@ -85,7 +87,10 @@ module Ufo
85
87
  end
86
88
 
87
89
  def helper
88
- Helper.new
90
+ helper = Helper.new
91
+ helper.add_project_helpers
92
+ helper
89
93
  end
94
+ memoize :helper
90
95
  end
91
96
  end
@@ -6,11 +6,20 @@
6
6
  # Simply aggregates a bunch of variables that is useful for the task_definition.
7
7
  module Ufo
8
8
  class DSL
9
- # provides some helperally context variables
10
9
  class Helper
11
10
  include Ufo::Util
12
11
  extend Memoist
13
12
 
13
+ # Add helpers from .ufo/helpers folder
14
+ def add_project_helpers
15
+ helpers_dir = "#{Ufo.root}/.ufo/helpers"
16
+ Dir.glob("#{helpers_dir}/**/*").each do |path|
17
+ next unless File.file?(path)
18
+ klass = path.gsub(%r{.*\.ufo/helpers/},'').sub(".rb",'').camelize
19
+ self.class.send(:include, klass.constantize)
20
+ end
21
+ end
22
+
14
23
  ##############
15
24
  # helper variables
16
25
  def dockerfile_port
@@ -27,48 +36,21 @@ module Ufo
27
36
 
28
37
  #############
29
38
  # helper methods
30
- def env_vars(text)
31
- lines = filtered_lines(text)
32
- lines.map do |line|
33
- key,*value = line.strip.split("=").map do |x|
34
- remove_surrounding_quotes(x.strip)
35
- end
36
- value = value.join('=')
37
- {
38
- name: key,
39
- value: value,
40
- }
41
- end
39
+ def env(text)
40
+ Vars.new(text: text).env
42
41
  end
42
+ alias_method :env_vars, :env
43
43
 
44
- def remove_surrounding_quotes(s)
45
- if s =~ /^"/ && s =~ /"$/
46
- s.sub(/^["]/, '').gsub(/["]$/,'') # remove surrounding double quotes
47
- elsif s =~ /^'/ && s =~ /'$/
48
- s.sub(/^[']/, '').gsub(/[']$/,'') # remove surrounding single quotes
49
- else
50
- s
51
- end
44
+ def env_file(path)
45
+ Vars.new(file: path).env
52
46
  end
53
47
 
54
- def filtered_lines(text)
55
- lines = text.split("\n")
56
- # remove comment at the end of the line
57
- lines.map! { |l| l.sub(/\s+#.*/,'').strip }
58
- # filter out commented lines
59
- lines = lines.reject { |l| l =~ /(^|\s)#/i }
60
- # filter out empty lines
61
- lines = lines.reject { |l| l.strip.empty? }
48
+ def secrets(text)
49
+ Vars.new(text: text, secrets: true).secrets
62
50
  end
63
51
 
64
- def env_file(path)
65
- full_path = "#{Ufo.root}/#{path}"
66
- unless File.exist?(full_path)
67
- puts "The #{full_path} env file could not be found. Are you sure it exists?"
68
- exit 1
69
- end
70
- text = IO.read(full_path)
71
- env_vars(text)
52
+ def secrets_file(path)
53
+ Vars.new(file: path, secrets: true).secrets
72
54
  end
73
55
 
74
56
  def current_region
@@ -0,0 +1,98 @@
1
+ require "aws_data"
2
+
3
+ class Ufo::DSL::Helper
4
+ class Vars
5
+ extend Memoist
6
+
7
+ def initialize(options={})
8
+ # use either file or text. text takes higher precedence
9
+ @file = options[:file]
10
+ @text = options[:text]
11
+ @secrets = options[:secrets] # true or false
12
+ end
13
+
14
+ def content
15
+ @text || read(@file)
16
+ end
17
+
18
+ def read(path)
19
+ full_path = "#{Ufo.root}/#{path}"
20
+ unless File.exist?(full_path)
21
+ puts "The #{full_path} env file could not be found. Are you sure it exists?"
22
+ exit 1
23
+ end
24
+ IO.read(full_path)
25
+ end
26
+
27
+ def env
28
+ lines = filtered_lines(content)
29
+ lines.map do |line|
30
+ key,*value = line.strip.split("=").map do |x|
31
+ remove_surrounding_quotes(x.strip)
32
+ end
33
+ value = value.join('=')
34
+ {
35
+ name: key,
36
+ value: value,
37
+ }
38
+ end
39
+ end
40
+
41
+ def secrets
42
+ secrets = env
43
+ secrets.map do |item|
44
+ value = item.delete(:value)
45
+ item[:valueFrom] = substitute(expand_secret(value))
46
+ end
47
+ secrets
48
+ end
49
+
50
+ def expand_secret(value)
51
+ case value
52
+ when /^ssm:/i
53
+ value.sub(/^ssm:/i, "arn:aws:ssm:#{region}:#{account}:parameter/")
54
+ when /^secretsmanager:/i
55
+ value.sub(/^secretsmanager:/i, "arn:aws:secretsmanager:#{region}:#{account}:secret:")
56
+ else
57
+ value # assume full arn has been passed
58
+ end
59
+ end
60
+
61
+ def substitute(value)
62
+ value.gsub(":UFO_ENV", Ufo.env)
63
+ end
64
+
65
+ def remove_surrounding_quotes(s)
66
+ if s =~ /^"/ && s =~ /"$/
67
+ s.sub(/^["]/, '').gsub(/["]$/,'') # remove surrounding double quotes
68
+ elsif s =~ /^'/ && s =~ /'$/
69
+ s.sub(/^[']/, '').gsub(/[']$/,'') # remove surrounding single quotes
70
+ else
71
+ s
72
+ end
73
+ end
74
+
75
+ def filtered_lines(content)
76
+ lines = content.split("\n")
77
+ # remove comment at the end of the line
78
+ lines.map! { |l| l.sub(/\s+#.*/,'').strip }
79
+ # filter out commented lines
80
+ lines = lines.reject { |l| l =~ /(^|\s)#/i }
81
+ # filter out empty lines
82
+ lines = lines.reject { |l| l.strip.empty? }
83
+ end
84
+
85
+ def aws_data
86
+ AwsData.new
87
+ end
88
+ memoize :aws_data
89
+
90
+ def region
91
+ aws_data.region
92
+ end
93
+
94
+ def account
95
+ aws_data.account
96
+ end
97
+ end
98
+ end
@@ -5,7 +5,6 @@ module Ufo
5
5
  @name = name
6
6
  @erb_result = erb_result
7
7
  @options = options
8
- @pretty = options[:pretty].nil? ? true : options[:pretty]
9
8
  end
10
9
 
11
10
  def write
@@ -16,25 +15,29 @@ module Ufo
16
15
  path = "#{output_path}/#{@name}.json".sub(/^\.\//,'')
17
16
  puts " #{path}" unless @options[:quiet]
18
17
  validate(@erb_result, path)
19
- json = @pretty ?
20
- JSON.pretty_generate(JSON.parse(@erb_result)) :
21
- @erb_result
22
- File.open(path, 'w') {|f| f.write(output_json(json)) }
18
+ data = JSON.parse(@erb_result)
19
+ override_image(data)
20
+ json = JSON.pretty_generate(data)
21
+ File.open(path, 'w') {|f| f.write(json) }
22
+ end
23
+
24
+ def override_image(data)
25
+ return data unless @options[:image_override]
26
+ data["containerDefinitions"].each do |container_definition|
27
+ container_definition["image"] = @options[:image_override]
28
+ end
23
29
  end
24
30
 
25
31
  def validate(json, path)
26
32
  begin
27
33
  JSON.parse(json)
28
34
  rescue JSON::ParserError => e
35
+ puts "#{e.class}: #{e.message}"
29
36
  puts "Invalid json. Output written to #{path} for debugging".color(:red)
30
37
  File.open(path, 'w') {|f| f.write(json) }
31
38
  exit 1
32
39
  end
33
40
  end
34
-
35
- def output_json(json)
36
- @options[:pretty] ? JSON.pretty_generate(JSON.parse(json)) : json
37
- end
38
41
  end
39
42
  end
40
43
  end
@@ -11,6 +11,7 @@ module Ufo
11
11
  def create
12
12
  puts "Ensuring log group for #{@task_definition.color(:green)} task definition exists"
13
13
  return if @options[:noop]
14
+ return if @options[:rollback] # dont need to create log group because previously deployed
14
15
 
15
16
  Ufo.check_task_definition!(@task_definition)
16
17
  task_def = JSON.load(IO.read(task_def_path))
@@ -0,0 +1,66 @@
1
+ module Ufo::Role
2
+ class Builder
3
+ def initialize(role_type)
4
+ @role_type = role_type
5
+ end
6
+
7
+ def build
8
+ resource(policies, managed_policy_arns)
9
+ end
10
+
11
+ def build?
12
+ !!(policies || managed_policy_arns)
13
+ end
14
+
15
+ def policies
16
+ items = Registry.policies[@role_type] # Array of Arrays
17
+ return unless items && !items.empty?
18
+
19
+ items.map do |item|
20
+ policy_name, statements = item # first element has policy name, second element has statements
21
+ {
22
+ PolicyName: policy_name,
23
+ PolicyDocument: {
24
+ Version: "2012-10-17",
25
+ Statement: statements
26
+ }
27
+ }
28
+ end
29
+ end
30
+
31
+ def managed_policy_arns
32
+ items = Registry.managed_policies[@role_type] # Array of Arrays
33
+ return unless items && !items.empty?
34
+
35
+ items.map do |item|
36
+ item.include?('iam::aws:policy') ? item : "arn:aws:iam::aws:policy/#{item}"
37
+ end
38
+ end
39
+
40
+ def resource(policies, managed_policy_arns)
41
+ properties = {
42
+ AssumeRolePolicyDocument: {
43
+ Version: "2012-10-17",
44
+ Statement: [
45
+ {
46
+ Effect: "Allow",
47
+ Principal: {
48
+ Service: "ecs-tasks.amazonaws.com"
49
+ },
50
+ Action: "sts:AssumeRole"
51
+ }
52
+ ]
53
+ },
54
+ }
55
+ properties[:Policies] = policies if policies
56
+ properties[:ManagedPolicyArns] = managed_policy_arns if managed_policy_arns
57
+
58
+ attrs = {
59
+ Type: "AWS::IAM::Role",
60
+ Properties: properties
61
+ }
62
+
63
+ attrs.deep_stringify_keys
64
+ end
65
+ end
66
+ end