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
@@ -2,13 +2,15 @@ class Ufo::Stack
2
2
  class Context
3
3
  extend Memoist
4
4
  include Helper
5
+ include Ufo::Settings
5
6
 
7
+ attr_reader :stack_name
6
8
  def initialize(options)
7
9
  @options = options
8
10
  @task_definition = options[:task_definition]
9
11
  @service = options[:service]
10
12
  # no need to adjust @cluster or @stack_name because it was adjusted in Stack#initialize
11
- @cluster = options[:cluster]
13
+ @cluster = options[:cluster].dup # Thor options are frozen, we thaw it because CustomProperties#substitute_variables does a sub!
12
14
  @stack_name = options[:stack_name]
13
15
 
14
16
  @stack = options[:stack]
@@ -20,36 +22,44 @@ class Ufo::Stack
20
22
  # Add additional variable to scope for CloudFormation template.
21
23
  # Dirties the scope but needed.
22
24
  vars = {
25
+ service: @service,
23
26
  cluster: @cluster,
24
27
  stack_name: @stack_name, # used in custom_properties
25
28
  container: container,
29
+ # to reconstruct TaskDefinition in the CloudFormation template
30
+ task_definition: @task_definition,
31
+ rollback_definition_arn: @options[:rollback_definition_arn],
26
32
  # elb options remember that their 'state'
27
33
  create_elb: create_elb?, # helps set Ecs DependsOn
28
34
  elb_type: elb_type,
29
35
  subnet_mappings: subnet_mappings,
30
- create_route53: create_elb? && cfn[:dns] && cfn[:dns][:name],
36
+ create_route53: create_elb? && has_dns_name?,
31
37
  default_target_group_protocol: default_target_group_protocol,
32
38
  default_listener_protocol: default_listener_protocol,
33
39
  default_listener_ssl_protocol: default_listener_ssl_protocol,
34
40
  create_listener_ssl: create_listener_ssl?,
35
41
  }
36
- # puts "vars:".color(:cyan)
37
- # pp vars
42
+
38
43
  scope.assign_instance_variables(vars)
39
44
  scope
40
45
  end
41
46
  memoize :scope
42
47
 
48
+ def has_dns_name?
49
+ cfn.dig(:Dns, :Name) || cfn.dig(:dns, :name) # backwards compatiblity
50
+ end
51
+
43
52
  def default_target_group_protocol
44
53
  return 'TCP' if elb_type == 'network'
45
54
  'HTTP'
46
55
  end
47
56
 
48
57
  def default_listener_protocol
58
+ port = cfn.dig(:Listener, :Port) || cfn.dig(:listener, :port) # backwards compatiblity
49
59
  if elb_type == 'network'
50
- cfn[:listener][:port] == 443 ? 'TLS' : 'TCP'
60
+ port == 443 ? 'TLS' : 'TCP'
51
61
  else
52
- cfn[:listener][:port] == 443 ? 'HTTPS' : 'HTTP'
62
+ port == 443 ? 'HTTPS' : 'HTTP'
53
63
  end
54
64
  end
55
65
 
@@ -59,32 +69,8 @@ class Ufo::Stack
59
69
 
60
70
  # if the configuration is set to anything then enable it
61
71
  def create_listener_ssl?
62
- cfn[:listener_ssl] && cfn[:listener_ssl][:port]
63
- end
64
-
65
- def container
66
- resp = ecs.describe_task_definition(task_definition: @task_definition)
67
- task_definition = resp.task_definition
68
-
69
- container_def = task_definition["container_definitions"].first
70
- requires_compatibilities = task_definition["requires_compatibilities"]
71
- fargate = requires_compatibilities && requires_compatibilities == ["FARGATE"]
72
- network_mode = task_definition["network_mode"]
73
-
74
- mappings = container_def["port_mappings"] || []
75
- unless mappings.empty?
76
- port = mappings.first["container_port"]
77
- end
78
-
79
- result = {
80
- name: container_def["name"],
81
- fargate: fargate,
82
- network_mode: network_mode, # awsvpc, bridge, etc
83
- }
84
- result[:port] = port if port
85
- result
72
+ cfn.dig(:ListenerSsl, :Port) || cfn.dig(:listener_ssl, :port) # backwards compatiblity
86
73
  end
87
- memoize :container
88
74
 
89
75
  def create_elb?
90
76
  create_elb, _ = elb_options
@@ -135,6 +121,29 @@ class Ufo::Stack
135
121
  [create_elb, elb_target_group]
136
122
  end
137
123
 
124
+ def container
125
+ task_definition = Builder::Resources::TaskDefinition::Reconstructor.new(@task_definition, @options[:rollback]).reconstruct
126
+
127
+ container_def = task_definition["ContainerDefinitions"].first
128
+ requires_compatibilities = task_definition["RequiresCompatibilities"]
129
+ fargate = requires_compatibilities && requires_compatibilities == ["FARGATE"]
130
+ network_mode = task_definition["NetworkMode"]
131
+
132
+ mappings = container_def["PortMappings"] || []
133
+ unless mappings.empty?
134
+ port = mappings.first["ContainerPort"]
135
+ end
136
+
137
+ result = {
138
+ name: container_def["Name"],
139
+ fargate: fargate,
140
+ network_mode: network_mode, # awsvpc, bridge, etc
141
+ }
142
+ result[:port] = port if port
143
+ result
144
+ end
145
+ memoize :container
146
+
138
147
  def get_parameter_value(stack, key)
139
148
  param = stack.parameters.find do |p|
140
149
  p.parameter_key == key
@@ -188,10 +197,8 @@ class Ufo::Stack
188
197
 
189
198
  def build_subnet_mappings!(allocations)
190
199
  unless allocations.size == network[:elb_subnets].size
191
- # puts "caller:".color(:cyan)
192
- # puts caller
193
200
  puts "ERROR: The allocation_ids must match in length to the subnets.".color(:red)
194
- puts "Please double check that .ufo/settings/network/#{settings[:network_profile]} has the same number of subnets as the eip allocation ids are you specifying."
201
+ puts "Please double check that .ufo/settings/network/#{settings.network_profile} has the same number of subnets as the eip allocation ids are you specifying."
195
202
  subnets = network[:elb_subnets]
196
203
  puts "Conigured subnets: #{subnets.inspect}"
197
204
  puts "Specified allocation ids: #{allocations.inspect}"
@@ -242,19 +249,5 @@ class Ufo::Stack
242
249
  end
243
250
  memoize :elb_type
244
251
 
245
- def network
246
- Ufo::Setting::Profile.new(:network, settings[:network_profile]).data
247
- end
248
- memoize :network
249
-
250
- def cfn
251
- Ufo::Setting::Profile.new(:cfn, settings[:cfn_profile]).data
252
- end
253
- memoize :cfn
254
-
255
- def settings
256
- Ufo.settings
257
- end
258
-
259
252
  end
260
253
  end
@@ -0,0 +1,59 @@
1
+ class Ufo::Stack
2
+ class CustomProperties
3
+ include Ufo::Settings
4
+
5
+ def initialize(template, stack_name)
6
+ @template, @stack_name = template, stack_name
7
+ end
8
+
9
+ def apply
10
+ customizations = camelize(cfn)
11
+ @template["Resources"].each do |logical_id, attrs|
12
+ custom_props = customizations[logical_id]
13
+ next unless custom_props
14
+ attrs["Properties"].deeper_merge!(custom_props, {overwrite_arrays: true})
15
+ end
16
+
17
+ substitute_variables!(@template["Resources"])
18
+ @template
19
+ end
20
+
21
+ # Keep backward compatiablity but encouraging CamelCase now because in the ufo init generator
22
+ # the .ufo/settings/cfn/default.yml is now CamelCase
23
+ def camelize(properties)
24
+ if ENV['UFO_CAMELIZE'] == '0' || settings[:auto_camelize] == false # provide a way to quickly test full camelize disable
25
+ return properties.deep_stringify_keys
26
+ end
27
+
28
+ # transform keys: camelize
29
+ properties.deep_stringify_keys.deep_transform_keys do |key|
30
+ if key == key.upcase # trying to generalize special rule for dns.TTL
31
+ key # leave key alone if key is already in all upcase
32
+ else
33
+ key.camelize
34
+ end
35
+ end
36
+ end
37
+
38
+ # Substitute special variables that cannot be baked into the template
39
+ # because they are dynamically assigned. Only one special variable:
40
+ #
41
+ # {stack_name}
42
+ def substitute_variables!(properties)
43
+ # transform values and substitute for special values
44
+ # https://stackoverflow.com/questions/34595142/process-nested-hash-to-convert-all-values-to-strings
45
+ #
46
+ # Examples:
47
+ # "{stack_name}.stag.boltops.com." => development-demo-web.stag.boltops.com.
48
+ # "{stack_name}.stag.boltops.com." => dev-demo-web-2.stag.boltops.com.
49
+ properties.deep_merge(properties) do |_,_,v|
50
+ if v.is_a?(String)
51
+ v.sub!('{stack_name}', @stack_name) # need shebang, updating in-place
52
+ else
53
+ v
54
+ end
55
+ end
56
+ properties
57
+ end
58
+ end
59
+ end
@@ -2,6 +2,7 @@ class Ufo::Stack
2
2
  module Helper
3
3
  include Ufo::AwsService
4
4
  include Ufo::Util
5
+ include Ufo::Settings
5
6
  extend Memoist
6
7
 
7
8
  def find_stack(stack_name)
@@ -34,15 +35,11 @@ class Ufo::Stack
34
35
  when "append_nothing", "prepend_nothing"
35
36
  [service, Ufo.env_extra]
36
37
  else # new default. ufo v4.5 and above
37
- [service, Ufo.env, Ufo.env_extra]
38
+ [service, Ufo.env.to_s, Ufo.env_extra]
38
39
  end
39
40
  parts.reject {|x| x==''}.compact.join('-') # stack_name
40
41
  end
41
42
 
42
- def cfn
43
- Ufo::Setting::Profile.new(:cfn, settings[:cfn_profile]).data
44
- end
45
-
46
43
  def status
47
44
  Status.new(@stack_name)
48
45
  end
@@ -0,0 +1,13 @@
1
+ class Ufo::Stack
2
+ class TemplateBody
3
+ def initialize(context)
4
+ @context = context
5
+ end
6
+
7
+ def build
8
+ builder = Builder.new(@context)
9
+ builder.build
10
+ end
11
+ end
12
+ end
13
+
@@ -2,8 +2,9 @@ module Ufo
2
2
  class Task < Base
3
3
  extend Memoist
4
4
 
5
- include Util
6
5
  include AwsService
6
+ include Ufo::Settings
7
+ include Util
7
8
 
8
9
  def initialize(task_definition, options)
9
10
  @task_definition = task_definition
@@ -139,12 +140,6 @@ module Ufo
139
140
  options
140
141
  end
141
142
 
142
- def network
143
- settings = Ufo.settings
144
- Setting::Profile.new(:network, settings[:network_profile]).data
145
- end
146
- memoize :network
147
-
148
143
  def cloudwatch_info(task_arn)
149
144
  config = container_definition[:log_configuration]
150
145
  container_name = container_definition[:name]
@@ -2,7 +2,7 @@ module Ufo
2
2
  class Tasks < Command
3
3
  desc "build", "Build task definitions."
4
4
  long_desc Help.text("tasks:build")
5
- option :pretty, type: :boolean, default: true, desc: "Pretty format the json for the task definitions"
5
+ option :image_override, desc: "Override image in task definition for quick testing"
6
6
  def build
7
7
  Tasks::Builder.new(options).build
8
8
  end
@@ -6,7 +6,6 @@ module Ufo
6
6
  # build and register task definitions. There is little point of running them independently
7
7
  # This method helps us do that.
8
8
  build(options)
9
- Tasks::Register.register(task_definition, options)
10
9
  end
11
10
 
12
11
  # ship: build and registers task definitions together
@@ -1,6 +1,7 @@
1
1
  module Ufo
2
2
  class TemplateScope
3
3
  extend Memoist
4
+ include Ufo::Settings
4
5
 
5
6
  attr_reader :helper
6
7
  attr_reader :task_definition_name
@@ -44,72 +45,6 @@ module Ufo
44
45
  end
45
46
  end
46
47
 
47
- def network
48
- Ufo::Setting::Profile.new(:network, settings[:network_profile]).data
49
- end
50
- memoize :network
51
-
52
- def cfn
53
- Ufo::Setting::Profile.new(:cfn, settings[:cfn_profile]).data
54
- end
55
- memoize :cfn
56
-
57
- def settings
58
- Ufo.settings
59
- end
60
-
61
- def custom_properties(resource)
62
- resource = resource.to_s.underscore
63
- properties = cfn[resource.to_sym]
64
- return unless properties
65
-
66
- # transform keys: camelize
67
- properties = properties.deep_stringify_keys.deep_transform_keys do |key|
68
- if key == key.upcase # trying to generalize special rule for dns.TTL
69
- key # leave key alone if key is already in all upcase
70
- else
71
- key.camelize
72
- end
73
- end
74
-
75
- substitute_variables!(properties)
76
-
77
- yaml = YAML.dump(properties)
78
- # add spaces in front on each line
79
- yaml.split("\n")[1..-1].map do |line|
80
- " #{line}"
81
- end.join("\n") + "\n"
82
- end
83
-
84
- # Substitute special variables that cannot be baked into the template
85
- # because they are dynamically assigned. Only one special variable:
86
- #
87
- # {stack_name}
88
- def substitute_variables!(properties)
89
- # transform values and substitute for special values
90
- # https://stackoverflow.com/questions/34595142/process-nested-hash-to-convert-all-values-to-strings
91
- #
92
- # Examples:
93
- # "{stack_name}.stag.boltops.com." => development-demo-web.stag.boltops.com.
94
- # "{stack_name}.stag.boltops.com." => dev-demo-web-2.stag.boltops.com.
95
- properties.deep_merge(properties) do |_,_,v|
96
- if v.is_a?(String)
97
- v.sub!('{stack_name}', @stack_name) # unsure why need shebang, but it works
98
- else
99
- v
100
- end
101
- end
102
- properties
103
- end
104
-
105
- def default_target_group_protocol
106
- default_elb_protocol
107
- end
108
-
109
- def default_elb_protocol
110
- @elb_type == "application" ? "HTTP" : "TCP"
111
- end
112
-
113
48
  def pretty_name?
114
49
  # env variable takes highest precedence
115
50
  if ENV["STATIC_NAME"]
@@ -0,0 +1,24 @@
1
+ module Ufo::Utils
2
+ class Squeezer
3
+ def initialize(data)
4
+ @data = data
5
+ end
6
+
7
+ def squeeze(new_data=nil)
8
+ data = new_data.nil? ? @data : new_data
9
+
10
+ case data
11
+ when Array
12
+ data.map! { |v| squeeze(v) }
13
+ when Hash
14
+ data.each_with_object({}) do |(k,v), squeezed|
15
+ # only remove nil and empty Array values within Hash structures
16
+ squeezed[k] = squeeze(v) unless v.nil? || v.is_a?(Array) && v.empty?
17
+ squeezed
18
+ end
19
+ else
20
+ data # do not transform
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module Ufo
2
- VERSION = "4.6.3"
2
+ VERSION = "5.0.0"
3
3
  end
@@ -0,0 +1,17 @@
1
+ iam_policy("AmazonS3ReadOnlyAccess",
2
+ Action: [
3
+ "s3:Get*",
4
+ "s3:List*"
5
+ ],
6
+ Effect: "Allow",
7
+ Resource: "*"
8
+ )
9
+ iam_policy("CloudwatchWrite",
10
+ Action: [
11
+ "cloudwatch:PutMetricData",
12
+ ],
13
+ Effect: "Allow",
14
+ Resource: "*"
15
+ )
16
+
17
+ managed_iam_policy("AmazonS3ReadOnlyAccess", "AmazonEC2ReadOnlyAccess")
@@ -0,0 +1,67 @@
1
+ describe Ufo::Role::Builder do
2
+ let(:builder) { described_class.new(role_type) }
3
+ let(:role_type) { "task_role" }
4
+
5
+ before(:each) do
6
+ Ufo::Role::Registry.register_policy("task_role",
7
+ "AmazonS3ReadOnlyAccess",
8
+ {:Action=>["s3:Get*", "s3:List*"], :Effect=>"Allow", :Resource=>"*"}
9
+ )
10
+ Ufo::Role::Registry.register_policy("task_role",
11
+ "CloudwatchWrite",
12
+ {:Action=>["cloudwatch:PutMetricData"], :Effect=>"Allow", :Resource=>"*"}
13
+ )
14
+ # Called twice on purpose to show that duplicated items in the set wont create doubles.
15
+ # This allows the DSL evaluate to be ran multiple times.
16
+ Ufo::Role::Registry.register_policy("task_role",
17
+ "CloudwatchWrite",
18
+ {:Action=>["cloudwatch:PutMetricData"], :Effect=>"Allow", :Resource=>"*"}
19
+ )
20
+
21
+
22
+ Ufo::Role::Registry.register_managed_policy("task_role",
23
+ "AmazonS3ReadOnlyAccess", "AmazonEC2ReadOnlyAccess"
24
+ )
25
+ end
26
+
27
+ context "build" do
28
+ it "builds role" do
29
+ resource = builder.build
30
+ expected = <<YAML
31
+ ---
32
+ Type: AWS::IAM::Role
33
+ Properties:
34
+ AssumeRolePolicyDocument:
35
+ Version: '2012-10-17'
36
+ Statement:
37
+ - Effect: Allow
38
+ Principal:
39
+ Service: ecs-tasks.amazonaws.com
40
+ Action: sts:AssumeRole
41
+ Policies:
42
+ - PolicyName: AmazonS3ReadOnlyAccess
43
+ PolicyDocument:
44
+ Version: '2012-10-17'
45
+ Statement:
46
+ - Action:
47
+ - s3:Get*
48
+ - s3:List*
49
+ Effect: Allow
50
+ Resource: "*"
51
+ - PolicyName: CloudwatchWrite
52
+ PolicyDocument:
53
+ Version: '2012-10-17'
54
+ Statement:
55
+ - Action:
56
+ - cloudwatch:PutMetricData
57
+ Effect: Allow
58
+ Resource: "*"
59
+ ManagedPolicyArns:
60
+ - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
61
+ - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess
62
+ YAML
63
+ yaml = YAML.dump(resource)
64
+ expect(yaml).to eq(expected)
65
+ end
66
+ end
67
+ end