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
@@ -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