OSDN Git Service

adds a reverse proxy service (Nginx) for the UI service SSL/TLS settings.
[metasearch/grid-chef-repo.git] / cookbooks / screwdriver / recipes / docker-compose.rb
1 #
2 # Cookbook Name:: screwdriver
3 # Recipe:: docker-compose
4 #
5 # Copyright 2017, whitestar
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 #     http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19
20 doc_url = 'https://hub.docker.com/r/screwdrivercd/screwdriver/'
21
22 #include_recipe 'platform_utils::kernel_user_namespace'
23 include_recipe 'docker-grid::compose'
24
25 app_dir = node['screwdriver']['docker-compose']['app_dir']
26 bin_dir = node['screwdriver']['docker-compose']['bin_dir']
27 config_dir = node['screwdriver']['docker-compose']['config_dir']
28 data_dir = node['screwdriver']['docker-compose']['data_dir']
29 etc_dir = node['screwdriver']['docker-compose']['etc_dir']
30
31 [
32   app_dir,
33   bin_dir,
34   config_dir,
35   data_dir,
36   "#{etc_dir}/nginx",
37 ].each {|dir|
38   resources(directory: dir) rescue directory dir do
39     owner 'root'
40     group 'root'
41     mode '0755'
42     recursive true
43   end
44 }
45
46 api_config_file = "#{config_dir}/api-local.yaml"
47 env_file = "#{app_dir}/.env"
48 config_file = "#{app_dir}/docker-compose.yml"
49
50 api_config_local = nil
51 if File.exist?(api_config_file)
52   require 'yaml'
53   api_config_local = YAML.load_file(api_config_file)
54 end
55
56 env_local = nil
57 if File.exist?(env_file)
58   env_local = {}
59   File.open(env_file) do |file|
60     file.each_line do |line|
61       env_local[$1] = $2 if line =~ /^([^=]*)=(.*)$/
62     end
63   end
64 end
65
66 =begin
67 config_srvs_local = nil
68 if File.exist?(config_file)
69   require 'yaml'
70   config_srvs_local = YAML.load_file(config_file)['services']
71 end
72 =end
73
74 # We use plain Hash objects instead of Chef attribute objects for containg secrets (JWT key pair).
75 override_api_config = node['screwdriver']['api']['config'].to_hash
76 override_store_config = node['screwdriver']['store']['config'].to_hash
77 #override_api_config = node.override['screwdriver']['api']['config']      # NG
78 #override_store_config = node.override['screwdriver']['store']['config']  # NG
79
80 config_srvs = node['screwdriver']['docker-compose']['config']['services']
81 override_config_srvs = node.override['screwdriver']['docker-compose']['config']['services']
82 force_override_config_srvs = node.force_override['screwdriver']['docker-compose']['config']['services']
83
84 # api
85 api_envs_org = config_srvs['api']['environment']
86 api_envs = {}
87 api_vols = config_srvs['api']['volumes'].to_a
88
89 api_port = '9001'  # default
90 api_in_port = api_envs_org['PORT']
91 ports = config_srvs['api']['ports']
92 if ports.empty?
93   override_config_srvs['api']['ports'] = ["#{api_port}:#{api_in_port}"]
94 else
95   ports.each {|port|
96     elms = port.split(':')
97     api_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == api_in_port
98   }
99 end
100 api_vols.push("#{data_dir}:/sd-data:rw")
101
102 jwt_private_key_reset = node['screwdriver']['docker-compose']['jwt_private_key_reset']
103 jwt_private_key = nil
104 jwt_public_key  = nil
105 jwt_private_key_vault_item = node['screwdriver']['docker-compose']['jwt_private_key_vault_item']
106 jwt_public_key_vault_item  = node['screwdriver']['docker-compose']['jwt_public_key_vault_item']
107
108 if !jwt_private_key_vault_item.empty?
109   # 1. from Chef Vault (recommended).
110   jwt_private_key = get_vault_item_value(jwt_private_key_vault_item)
111   jwt_public_key  = get_vault_item_value(jwt_public_key_vault_item)
112   log 'JWT key pair has been loaded from Chef Vault.'
113 else
114   # 2. from Chef attribute (NOT recommended).
115   jwt_private_key = api_envs_org['SECRET_JWT_PRIVATE_KEY']
116   jwt_public_key  = api_envs_org['SECRET_JWT_PUBLIC_KEY']
117   if jwt_private_key.nil? || jwt_private_key.empty?
118     if !api_config_local.nil? && !api_config_local['auth']['jwtPrivateKey'].nil? && !jwt_private_key_reset
119       # 3. preserve it from the local config/api-local.yaml file.
120       jwt_private_key = api_config_local['auth']['jwtPrivateKey']
121       jwt_public_key  = api_config_local['auth']['jwtPublicKey']
122       log 'JWT key pair is preserved from the local config/api-local.yaml file.'
123     # if !env_local.nil? && !env_local['SECRET_JWT_PRIVATE_KEY'].nil? && !jwt_private_key_reset
124     #   # 3. preserve it from the local .env file.
125     #   # Note: Docker env file format does not support backslash escaped string yet.
126     #   eval "jwt_private_key = %Q(#{env_local['SECRET_JWT_PRIVATE_KEY']})"
127     #   eval "jwt_public_key  = %Q(#{env_local['SECRET_JWT_PUBLIC_KEY']})"
128     #   log 'JWT key pair is preserved from the local .env file.'
129     else
130       # 4. auto generate.
131       require 'openssl'
132       rsa = OpenSSL::PKey::RSA.generate(2048)
133       jwt_private_key = rsa.export
134       jwt_public_key  = rsa.public_key.export
135       log 'JWT key pair has been generated.'
136     end
137   end
138 end
139
140 override_api_config['auth']['jwtPrivateKey'] = jwt_private_key
141 override_api_config['auth']['jwtPublicKey'] = jwt_public_key
142 # Note: prevent Chef from logging JWT key attribute values. (=> template variables)
143 # However Docker env file format does not support multi-line value and backslash escaped string yet.
144 #api_envs['SECRET_JWT_PRIVATE_KEY'] = '${SECRET_JWT_PRIVATE_KEY}'  # Useless
145 #api_envs['SECRET_JWT_PUBLIC_KEY']  = '${SECRET_JWT_PUBLIC_KEY}'   # Useless
146 #api_envs['SECRET_JWT_PRIVATE_KEY'] = jwt_private_key  # NG
147 #api_envs['SECRET_JWT_PUBLIC_KEY']  = jwt_public_key   # NG
148
149 cookie_password = nil
150 cookie_password_vault_item = node['screwdriver']['docker-compose']['cookie_password_vault_item']
151 unless cookie_password_vault_item.empty?
152   cookie_password = get_vault_item_value(cookie_password_vault_item)
153   api_envs['SECRET_COOKIE_PASSWORD'] = '${SECRET_COOKIE_PASSWORD}'
154 end
155
156 password = nil
157 password_vault_item = node['screwdriver']['docker-compose']['password_vault_item']
158 unless password_vault_item.empty?
159   password = get_vault_item_value(password_vault_item)
160   api_envs['SECRET_PASSWORD'] = '${SECRET_PASSWORD}'
161 end
162
163 oauth_client_id = nil
164 oauth_client_id_vault_item = node['screwdriver']['docker-compose']['oauth_client_id_vault_item']
165 unless oauth_client_id_vault_item.empty?
166   oauth_client_id = get_vault_item_value(oauth_client_id_vault_item)
167   api_envs['SECRET_OAUTH_CLIENT_ID'] = '${SECRET_OAUTH_CLIENT_ID}'
168 end
169
170 oauth_client_secret = nil
171 oauth_client_secret_vault_item = node['screwdriver']['docker-compose']['oauth_client_secret_vault_item']
172 unless oauth_client_secret_vault_item.empty?
173   oauth_client_secret = get_vault_item_value(oauth_client_secret_vault_item)
174   api_envs['SECRET_OAUTH_CLIENT_SECRET'] = '${SECRET_OAUTH_CLIENT_SECRET}'
175 end
176
177 webhook_github_secret = nil
178 webhook_github_secret_vault_item = node['screwdriver']['docker-compose']['webhook_github_secret_vault_item']
179 unless webhook_github_secret_vault_item.empty?
180   webhook_github_secret = get_vault_item_value(webhook_github_secret_vault_item)
181   api_envs['WEBHOOK_GITHUB_SECRET'] = '${WEBHOOK_GITHUB_SECRET}'
182 end
183
184 # ui
185 #ui_envs_org = config_srvs['ui']['environment']
186 ui_envs = {}
187 ui_vols = config_srvs['ui']['volumes'].to_a
188
189 ui_port = '9000'  # default
190 ui_in_port = '80'
191 ports = config_srvs['ui']['ports']
192 if ports.empty?
193   override_config_srvs['ui']['ports'] = ["#{ui_port}:#{ui_in_port}"]
194 else
195   ports.each {|port|
196     elms = port.split(':')
197     ui_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == ui_in_port
198   }
199 end
200
201 # store
202 store_envs_org = config_srvs['store']['environment']
203 store_envs = {}
204 store_vols = config_srvs['store']['volumes'].to_a
205
206 store_port = '9002'  # default
207 store_in_port = store_envs_org['PORT']
208 ports = config_srvs['store']['ports']
209 if ports.empty?
210   override_config_srvs['store']['ports'] = ["#{store_port}:#{store_in_port}"]
211 else
212   ports.each {|port|
213     elms = port.split(':')
214     store_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == store_in_port
215   }
216 end
217
218 override_store_config['auth']['jwtPublicKey'] = jwt_public_key
219 # Note: prevent Chef from logging JWT key attribute value. (=> template variables)
220 # However Docker env file format does not support multi-line value and backslash escaped string yet.
221 #store_envs['SECRET_JWT_PUBLIC_KEY'] = '${SECRET_JWT_PUBLIC_KEY}'  # Useless
222 #store_envs['SECRET_JWT_PUBLIC_KEY']  = jwt_public_key  # NG
223
224 api_uri = api_envs_org['URI']
225 store_uri = store_envs_org['URI']
226 ui_uri = api_uri.gsub(/:\d+/, ":#{ui_port}")  # based on the API URI.
227
228 if node['screwdriver']['with_ssl_cert_cookbook']
229   include_recipe 'ssl_cert::server_key_pairs'
230   ::Chef::Recipe.send(:include, SSLCert::Helper)
231
232   cn = node['screwdriver']['ssl_cert']['common_name']
233   server_cert = server_cert_content(cn)
234   server_key = server_key_content(cn)
235
236   api_uri = api_uri.gsub('http://', 'https://')
237   store_uri = store_uri.gsub('http://', 'https://')
238
239   override_api_config['httpd']['tls'] = {}  # for FalseClass by default.
240   override_api_config['httpd']['tls']['cert'] = server_cert
241   override_api_config['httpd']['tls']['key'] = server_key
242   api_envs['IS_HTTPS'] = 'true'
243
244   override_store_config['httpd']['tls'] = {}  # for FalseClass by default.
245   override_store_config['httpd']['tls']['cert'] = server_cert
246   override_store_config['httpd']['tls']['key'] = server_key
247
248   # Note: Screwdriver UI image does not support TLS settings yet.
249   # https://github.com/screwdriver-cd/screwdriver/issues/377
250
251   if node['screwdriver']['ui']['tls_setup_mode'] == 'reverseproxy'
252     rproxy_in_port = '9000'
253     ports = config_srvs['reverseproxy']['ports']
254     if ports.empty?
255       override_config_srvs['reverseproxy']['ports'] = ["#{ui_port}:#{rproxy_in_port}"]
256     else
257       ports.each {|port|
258         elms = port.split(':')
259         ui_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == rproxy_in_port
260       }
261     end
262     ui_uri = api_uri.gsub(/:\d+/, ":#{ui_port}")  # based on the API URI.
263     # do not expose UI service directly.
264     node.rm('screwdriver', 'docker-compose', 'config', 'services', 'ui', 'ports')
265
266     rproxy_vols = config_srvs['reverseproxy']['volumes'].to_a
267     rproxy_vols.push("#{etc_dir}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro")
268     # Nginx parent process owner is root.
269     rproxy_vols.push("#{server_cert_path(cn)}:/root/server.crt:ro")
270     rproxy_vols.push("#{server_key_path(cn)}:/root/server.key:ro")
271     # reset vlumes array.
272     override_config_srvs['reverseproxy']['volumes'] = rproxy_vols
273
274     template "#{etc_dir}/nginx/nginx.conf" do
275       source 'opt/docker-compose/app/screwdriver/etc/nginx/nginx.conf'
276       owner 'root'
277       group 'root'
278       mode '0644'
279       action :create
280     end
281   else
282     node.rm('screwdriver', 'docker-compose', 'config', 'services', 'reverseproxy')
283     # TODO: in the future.
284   end
285 else
286   node.rm('screwdriver', 'docker-compose', 'config', 'services', 'reverseproxy')
287 end
288
289 # normalize URIs
290 api_envs['URI'] = api_uri
291 api_envs['ECOSYSTEM_STORE'] = store_uri
292 api_envs['ECOSYSTEM_UI'] = ui_uri
293
294 ui_envs['ECOSYSTEM_API'] = api_uri
295 ui_envs['ECOSYSTEM_STORE'] = store_uri
296
297 store_envs['URI'] = store_uri
298 store_envs['ECOSYSTEM_UI'] = ui_uri
299
300 # Common
301 if node['screwdriver']['docker-compose']['import_ca']
302   include_recipe 'ssl_cert::ca_certs'
303   ::Chef::Recipe.send(:include, SSLCert::Helper)
304
305   node['screwdriver']['ssl_cert']['ca_names'].each {|ca_name|
306     ca_cert_vol = "#{ca_cert_path(ca_name)}:/usr/share/ca-certificates/#{ca_name}.crt:ro"
307     api_vols.push(ca_cert_vol)
308     #ui_vols.push(ca_cert_vol)
309   }
310
311   import_ca_script = '/usr/local/bin/screwdriver_import_ca'
312   template "#{bin_dir}/screwdriver_import_ca" do
313     source 'opt/docker-compose/app/screwdriver/bin/screwdriver_import_ca'
314     owner 'root'
315     group 'root'
316     mode '0755'
317     action :create
318   end
319   import_ca_script_vol = "#{bin_dir}/screwdriver_import_ca:#{import_ca_script}:ro"
320   api_vols.push(import_ca_script_vol)
321   #ui_vols.push(import_ca_script_vol)
322
323   api_command = config_srvs['api']['command']
324   override_config_srvs['api']['command'] \
325     = "/bin/sh -c \"#{import_ca_script} && #{api_command}\""
326 end
327
328 [
329   'api',
330   'store',
331 ].each {|srv|
332   local_yaml_file = "#{config_dir}/#{srv}-local.yaml"
333   srv_vols = nil
334   srv_config = nil
335   if srv == 'api'
336     srv_vols = api_vols
337     srv_config = override_api_config
338   elsif srv == 'store'
339     srv_vols = store_vols
340     srv_config = override_store_config
341   end
342
343   template local_yaml_file do
344     source "opt/docker-compose/app/screwdriver/config/#{srv}-local.yaml"
345     owner 'root'
346     group 'root'
347     mode '0600'
348     sensitive true
349     # prevent Chef from logging password attribute value.
350     variables(
351       config: srv_config
352     )
353   end
354
355   srv_vols.push("#{local_yaml_file}:/config/local.yaml:ro")
356 }
357
358 # merge environment hash
359 force_override_config_srvs['api']['environment'] = api_envs unless api_envs.empty?
360 force_override_config_srvs['ui']['environment'] = ui_envs unless ui_envs.empty?
361 force_override_config_srvs['store']['environment'] = store_envs unless store_envs.empty?
362 # reset vlumes array.
363 override_config_srvs['api']['volumes'] = api_vols unless api_vols.empty?
364 override_config_srvs['ui']['volumes'] = ui_vols unless ui_vols.empty?
365 override_config_srvs['store']['volumes'] = store_vols unless store_vols.empty?
366
367 template env_file do
368   source 'opt/docker-compose/app/screwdriver/.env'
369   owner 'root'
370   group 'root'
371   mode '0600'
372   sensitive true
373   # prevent Chef from logging password attribute value.
374   variables(
375     # secrets
376     # JWT keys setting -> /config/local.yaml
377     #jwt_private_key: jwt_private_key,
378     #jwt_public_key: jwt_public_key,
379     cookie_password: cookie_password,
380     password: password,
381     oauth_client_id: oauth_client_id,
382     oauth_client_secret: oauth_client_secret,
383     webhook_github_secret: webhook_github_secret
384   )
385 end
386
387 template config_file do
388   source 'opt/docker-compose/app/screwdriver/docker-compose.yml'
389   owner 'root'
390   group 'root'
391   mode '0644'
392 end
393
394 log <<-"EOM"
395 Note: You must execute the following command manually.
396   See #{doc_url}
397   * Start:
398     $ cd #{app_dir}
399     $ sudo docker-compose up -d
400   * Stop
401     $ sudo docker-compose down
402 EOM