ufo 4.6.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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