2 # Cookbook Name:: screwdriver
3 # Recipe:: docker-compose
5 # Copyright 2017-2018, whitestar
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
11 # http://www.apache.org/licenses/LICENSE-2.0
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.
20 require 'securerandom'
22 doc_url = 'https://hub.docker.com/r/screwdrivercd/screwdriver/'
24 ::Chef::Recipe.send(:include, SSLCert::Helper)
26 #include_recipe 'platform_utils::kernel_user_namespace'
27 include_recipe 'docker-grid::compose'
29 app_dir = node['screwdriver']['docker-compose']['app_dir']
30 bin_dir = node['screwdriver']['docker-compose']['bin_dir']
31 config_dir = node['screwdriver']['docker-compose']['config_dir']
32 data_dir = node['screwdriver']['docker-compose']['data_dir']
33 etc_dir = node['screwdriver']['docker-compose']['etc_dir']
42 resources(directory: dir) rescue directory dir do
50 api_config_file = "#{config_dir}/api-local.yaml"
51 env_file = "#{app_dir}/.env"
52 config_file = "#{app_dir}/docker-compose.yml"
54 api_config_local = nil
55 if File.exist?(api_config_file)
57 api_config_local = YAML.load_file(api_config_file)
61 if File.exist?(env_file)
62 File.open(env_file) do |file|
63 file.each_line do |line|
64 env_local[$1] = $2 if line =~ /^([^=]*)=(.*)$/
70 config_srvs_local = nil
71 if File.exist?(config_file)
73 config_srvs_local = YAML.load_file(config_file)['services']
77 # We use plain Hash objects instead of Chef attribute objects for containg secrets (JWT key pair).
78 override_api_config = node['screwdriver']['api']['config'].to_hash
79 override_store_config = node['screwdriver']['store']['config'].to_hash
80 #override_api_config = node.override['screwdriver']['api']['config'] # NG
81 #override_store_config = node.override['screwdriver']['store']['config'] # NG
83 config_srvs = node['screwdriver']['docker-compose']['config']['services']
84 override_config_srvs = node.override['screwdriver']['docker-compose']['config']['services']
85 force_override_config_srvs = node.force_override['screwdriver']['docker-compose']['config']['services']
88 api_envs_org = config_srvs['api']['environment']
90 api_vols = config_srvs['api']['volumes'].to_a
93 api_port = '9001' # default
94 api_in_port = api_envs_org['PORT']
95 ports = config_srvs['api']['ports']
97 override_config_srvs['api']['ports'] = ["#{api_port}:#{api_in_port}"]
100 elms = port.split(':')
101 api_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == api_in_port
106 'jwt_private_key_vault_item',
107 'jwt_public_key_vault_item',
108 'cookie_password_vault_item',
109 'password_vault_item',
111 # for backward compatibility.
112 if node['screwdriver'][vault_item].empty? && !node['screwdriver']['docker-compose'][vault_item].empty?
113 node.force_override['screwdriver'][vault_item] = node['screwdriver']['docker-compose'][vault_item].to_hash
117 jwt_private_key_reset = node['screwdriver']['docker-compose']['jwt_private_key_reset']
118 jwt_private_key = nil
120 jwt_private_key_vault_item = node['screwdriver']['jwt_private_key_vault_item']
121 jwt_public_key_vault_item = node['screwdriver']['jwt_public_key_vault_item']
123 if !jwt_private_key_vault_item.empty?
124 # 1. from Chef Vault (recommended).
125 jwt_private_key = get_vault_item_value(jwt_private_key_vault_item)
126 jwt_public_key = get_vault_item_value(jwt_public_key_vault_item)
127 log 'JWT key pair has been loaded from Chef Vault.'
129 # 2. from Chef attribute (NOT recommended).
130 jwt_private_key = api_envs_org['SECRET_JWT_PRIVATE_KEY']
131 jwt_public_key = api_envs_org['SECRET_JWT_PUBLIC_KEY']
132 if jwt_private_key.nil? || jwt_private_key.empty?
133 if !api_config_local.nil? && !api_config_local['auth']['jwtPrivateKey'].nil? && !jwt_private_key_reset
134 # 3. preserve it from the local config/api-local.yaml file.
135 jwt_private_key = api_config_local['auth']['jwtPrivateKey']
136 jwt_public_key = api_config_local['auth']['jwtPublicKey']
137 log 'JWT key pair is preserved from the local config/api-local.yaml file.'
138 # if !env_local['SECRET_JWT_PRIVATE_KEY'].nil? && !jwt_private_key_reset
139 # # 3. preserve it from the local .env file.
140 # # Note: Docker env file format does not support backslash escaped string yet.
141 # eval "jwt_private_key = %Q(#{env_local['SECRET_JWT_PRIVATE_KEY']})"
142 # eval "jwt_public_key = %Q(#{env_local['SECRET_JWT_PUBLIC_KEY']})"
143 # log 'JWT key pair is preserved from the local .env file.'
147 rsa = OpenSSL::PKey::RSA.generate(2048)
148 jwt_private_key = rsa.export
149 jwt_public_key = rsa.public_key.export
150 log 'JWT key pair has been generated.'
155 override_api_config['auth']['jwtPrivateKey'] = jwt_private_key
156 override_api_config['auth']['jwtPublicKey'] = jwt_public_key
157 # Note: prevent Chef from logging JWT key attribute values. (=> template variables)
158 # However Docker env file format does not support multi-line value and backslash escaped string yet.
159 #api_envs['SECRET_JWT_PRIVATE_KEY'] = '${SECRET_JWT_PRIVATE_KEY}' # Useless
160 #api_envs['SECRET_JWT_PUBLIC_KEY'] = '${SECRET_JWT_PUBLIC_KEY}' # Useless
161 #api_envs['SECRET_JWT_PRIVATE_KEY'] = jwt_private_key # NG
162 #api_envs['SECRET_JWT_PUBLIC_KEY'] = jwt_public_key # NG
164 cookie_password = nil
165 cookie_password_vault_item = node['screwdriver']['cookie_password_vault_item']
166 unless cookie_password_vault_item.empty?
167 cookie_password = get_vault_item_value(cookie_password_vault_item)
168 api_envs['SECRET_COOKIE_PASSWORD'] = '${SECRET_COOKIE_PASSWORD}'
172 password_vault_item = node['screwdriver']['password_vault_item']
173 unless password_vault_item.empty?
174 password = get_vault_item_value(password_vault_item)
175 api_envs['SECRET_PASSWORD'] = '${SECRET_PASSWORD}'
178 hashing_password = nil
179 # for backward compatibility
180 hashing_password = env_local['SECRET_PASSWORD'] if env_local['SECRET_HASHING_PASSWORD'].nil? && !env_local['SECRET_PASSWORD'].nil?
181 hashing_password_vault_item = node['screwdriver']['hashing_password_vault_item']
182 hashing_password = get_vault_item_value(hashing_password_vault_item) unless hashing_password_vault_item.empty?
183 api_envs['SECRET_HASHING_PASSWORD'] = '${SECRET_HASHING_PASSWORD}' unless hashing_password.nil?
185 node['screwdriver']['api']['scms_vault_items'].each {|scm, props|
186 props.each {|prop, vault_item|
187 unless vault_item.empty?
188 secret = get_vault_item_value(vault_item)
189 override_api_config['scms'][scm]['config'][prop] = secret
195 oauth_client_id = nil
196 oauth_client_id_vault_item = node['screwdriver']['docker-compose']['oauth_client_id_vault_item']
197 unless oauth_client_id_vault_item.empty?
198 oauth_client_id = get_vault_item_value(oauth_client_id_vault_item)
199 api_envs['SECRET_OAUTH_CLIENT_ID'] = '${SECRET_OAUTH_CLIENT_ID}'
202 oauth_client_secret = nil
203 oauth_client_secret_vault_item = node['screwdriver']['docker-compose']['oauth_client_secret_vault_item']
204 unless oauth_client_secret_vault_item.empty?
205 oauth_client_secret = get_vault_item_value(oauth_client_secret_vault_item)
206 api_envs['SECRET_OAUTH_CLIENT_SECRET'] = '${SECRET_OAUTH_CLIENT_SECRET}'
209 webhook_github_secret = nil
210 webhook_github_secret_vault_item = node['screwdriver']['docker-compose']['webhook_github_secret_vault_item']
211 unless webhook_github_secret_vault_item.empty?
212 webhook_github_secret = get_vault_item_value(webhook_github_secret_vault_item)
213 api_envs['WEBHOOK_GITHUB_SECRET'] = '${WEBHOOK_GITHUB_SECRET}'
218 db_username = env_local['DB_USERNAME'] unless env_local['DB_USERNAME'].nil?
219 db_username_vault_item = node['screwdriver']['db_username_vault_item']
220 db_username = get_vault_item_value(db_username_vault_item) unless db_username_vault_item.empty?
221 db_username = 'sd-admin' if db_username.nil?
222 api_envs['DATASTORE_SEQUELIZE_USERNAME'] = '${DB_USERNAME}'
225 db_password = env_local['DB_PASSWORD'] unless env_local['DB_PASSWORD'].nil?
226 db_password_vault_item = node['screwdriver']['db_password_vault_item']
227 db_password = get_vault_item_value(db_password_vault_item) unless db_password_vault_item.empty?
228 db_password = SecureRandom.urlsafe_base64(32) if db_password.nil?
229 api_envs['DATASTORE_SEQUELIZE_PASSWORD'] = '${DB_PASSWORD}'
231 db_root_password = nil
232 db_root_password = env_local['DB_ROOT_PASSWORD'] unless env_local['DB_ROOT_PASSWORD'].nil?
233 db_root_password_vault_item = node['screwdriver']['db_root_password_vault_item']
234 db_root_password = get_vault_item_value(db_root_password_vault_item) unless db_root_password_vault_item.empty?
235 db_root_password = SecureRandom.urlsafe_base64(32) if db_root_password.nil?
237 db_dialect = api_envs_org['DATASTORE_SEQUELIZE_DIALECT']
240 api_vols.push("#{data_dir}:/sd-data:rw")
241 api_envs['DATASTORE_SEQUELIZE_STORAGE'] = '/sd-data/storage.db'
242 when 'mysql', 'postgres'
244 api_envs['DATASTORE_SEQUELIZE_HOST'] = 'db'
248 if db_dialect != 'sqlite'
249 #db_envs_org = config_srvs['db']['environment']
251 db_vols = config_srvs['db']['volumes'].to_a
255 mysql_data_dir = "#{data_dir}/mysql"
256 resources(directory: mysql_data_dir) rescue directory mysql_data_dir do
263 db_vols.push("#{mysql_data_dir}:/var/lib/mysql:rw")
264 db_envs['MYSQL_DATABASE'] = api_envs_org['DATASTORE_SEQUELIZE_DATABASE']
265 db_envs['MYSQL_USER'] = '${DB_USERNAME}' unless db_username.nil?
266 db_envs['MYSQL_PASSWORD'] = '${DB_PASSWORD}' unless db_password.nil?
267 db_envs['MYSQL_ROOT_PASSWORD'] = '${DB_ROOT_PASSWORD}' unless db_root_password.nil?
269 pg_data_dir = "#{data_dir}/postgres"
270 resources(directory: pg_data_dir) rescue directory pg_data_dir do
277 db_vols.push("#{pg_data_dir}:/database:rw")
278 db_envs['POSTGRES_DB'] = api_envs_org['DATASTORE_SEQUELIZE_DATABASE']
279 db_envs['POSTGRES_USER'] = '${DB_USERNAME}' unless db_username.nil?
280 db_envs['POSTGRES_PASSWORD'] = '${DB_PASSWORD}' unless db_password.nil?
281 db_envs['PGDATA'] = '/database'
286 executor_queue_enabled = api_envs_org['EXECUTOR_QUEUE_ENABLED'] == 'true' ? true : false
287 if executor_queue_enabled
289 queue_vols = config_srvs['queue']['volumes'].to_a
291 api_links.push('queue')
292 api_envs['QUEUE_REDIS_HOST'] = 'queue'
293 # TODO: set up env. vars.
295 # QUEUE_REDIS_PASSWORD
296 # QUEUE_REDIS_TLS_ENABLED
297 # QUEUE_REDIS_DATABASE
300 override_config_srvs['api']['links'] = api_links unless api_links.empty?
303 #ui_envs_org = config_srvs['ui']['environment']
305 ui_vols = config_srvs['ui']['volumes'].to_a
307 ui_port = '9000' # default
309 ports = config_srvs['ui']['ports']
311 override_config_srvs['ui']['ports'] = ["#{ui_port}:#{ui_in_port}"]
314 elms = port.split(':')
315 ui_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == ui_in_port
320 store_backend = node['screwdriver']['store']['backend']
321 store_envs_org = config_srvs['store']['environment']
323 store_vols = config_srvs['store']['volumes'].to_a
326 store_port = '9002' # default
327 store_in_port = store_envs_org['PORT']
328 ports = config_srvs['store']['ports']
330 override_config_srvs['store']['ports'] = ["#{store_port}:#{store_in_port}"]
333 elms = port.split(':')
334 store_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == store_in_port
338 s3_access_key_id = nil
339 s3_access_key_id = env_local['S3_ACCESS_KEY_ID'] unless env_local['S3_ACCESS_KEY_ID'].nil?
340 s3_access_key_id_vault_item = node['screwdriver']['s3_access_key_id_vault_item']
341 s3_access_key_id = get_vault_item_value(s3_access_key_id_vault_item) unless s3_access_key_id_vault_item.empty?
342 s3_access_key_id = SecureRandom.urlsafe_base64(16) if s3_access_key_id.nil?
343 store_envs['S3_ACCESS_KEY_ID'] = '${S3_ACCESS_KEY_ID}'
345 s3_access_key_secret = nil
346 s3_access_key_secret = env_local['S3_ACCESS_KEY_SECRET'] unless env_local['S3_ACCESS_KEY_SECRET'].nil?
347 s3_access_key_secret_vault_item = node['screwdriver']['s3_access_key_secret_vault_item']
348 s3_access_key_secret = get_vault_item_value(s3_access_key_secret_vault_item) unless s3_access_key_secret_vault_item.empty?
349 s3_access_key_secret = SecureRandom.urlsafe_base64(32) if s3_access_key_secret.nil?
350 store_envs['S3_ACCESS_KEY_SECRET'] = '${S3_ACCESS_KEY_SECRET}'
352 # S3 compatible server
353 if !store_backend.nil? && !store_backend.empty?
354 store_links.push('screwdriver.s3')
355 store_envs['STRATEGY'] = 's3'
356 store_envs['S3_BUCKET'] = 'screwdriver'
358 #s3_envs_org = config_srvs['screwdriver.s3']['environment']
360 s3_vols = config_srvs['screwdriver.s3']['volumes'].to_a
362 s3_port = '9010' # default
364 ports = config_srvs['screwdriver.s3']['ports']
368 store_envs['S3_REGION'] = 'us-east-1'
369 store_envs['S3_ENDPOINT'] = "http://s3:#{s3_in_port}/screwdriver" # for path style
370 store_envs['S3_SIG_VER'] = 'v4'
373 override_config_srvs['screwdriver.s3']['ports'] = ["#{s3_port}:#{s3_in_port}"]
376 elms = port.split(':')
377 s3_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == s3_in_port
381 minio_data_dir = "#{data_dir}/minio"
382 resources(directory: minio_data_dir) rescue directory minio_data_dir do
389 s3_vols.push("#{minio_data_dir}:/export:rw")
390 s3_envs['MINIO_ACCESS_KEY'] = '${S3_ACCESS_KEY_ID}' unless s3_access_key_id.nil?
391 s3_envs['MINIO_SECRET_KEY'] = '${S3_ACCESS_KEY_SECRET}' unless s3_access_key_secret.nil?
395 override_config_srvs['store']['links'] = store_links unless store_links.empty?
397 override_store_config['auth']['jwtPublicKey'] = jwt_public_key
398 # Note: prevent Chef from logging JWT key attribute value. (=> template variables)
399 # However Docker env file format does not support multi-line value and backslash escaped string yet.
400 #store_envs['SECRET_JWT_PUBLIC_KEY'] = '${SECRET_JWT_PUBLIC_KEY}' # Useless
401 #store_envs['SECRET_JWT_PUBLIC_KEY'] = jwt_public_key # NG
403 api_uri = api_envs_org['URI']
404 store_uri = store_envs_org['URI']
405 ui_uri = api_uri.gsub(/:\d+/, ":#{ui_port}") # based on the API URI.
407 if node['screwdriver']['with_ssl_cert_cookbook']
408 cn = node['screwdriver']['ssl_cert']['common_name']
409 append_server_ssl_cn(cn)
410 include_recipe 'ssl_cert::server_key_pairs'
412 server_cert = server_cert_content(cn)
413 server_key = server_key_content(cn)
415 api_uri = api_uri.gsub('http://', 'https://')
416 store_uri = store_uri.gsub('http://', 'https://')
418 override_api_config['httpd']['tls'] = {} # for FalseClass by default.
419 override_api_config['httpd']['tls']['cert'] = server_cert
420 override_api_config['httpd']['tls']['key'] = server_key
421 api_envs['IS_HTTPS'] = 'true'
423 override_store_config['httpd']['tls'] = {} # for FalseClass by default.
424 override_store_config['httpd']['tls']['cert'] = server_cert
425 override_store_config['httpd']['tls']['key'] = server_key
427 # Note: Screwdriver UI image does not support TLS settings yet.
428 # https://github.com/screwdriver-cd/screwdriver/issues/377
430 if node['screwdriver']['ui']['tls_setup_mode'] == 'reverseproxy'
431 rproxy_in_port = '9000'
432 ports = config_srvs['reverseproxy']['ports']
434 override_config_srvs['reverseproxy']['ports'] = ["#{ui_port}:#{rproxy_in_port}"]
437 elms = port.split(':')
438 ui_port = (elms.size == 2 ? elms[0] : elms[1]) if elms.last == rproxy_in_port
441 ui_uri = api_uri.gsub(/:\d+/, ":#{ui_port}") # based on the API URI.
442 # do not expose UI service directly.
443 node.rm('screwdriver', 'docker-compose', 'config', 'services', 'ui', 'ports')
445 rproxy_vols = config_srvs['reverseproxy']['volumes'].to_a
446 rproxy_vols.push("#{etc_dir}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro")
447 # Nginx parent process owner is root.
448 rproxy_vols.push("#{server_cert_path(cn)}:/root/server.crt:ro")
449 rproxy_vols.push("#{server_key_path(cn)}:/root/server.key:ro")
450 # reset vlumes array.
451 override_config_srvs['reverseproxy']['volumes'] = rproxy_vols
453 template "#{etc_dir}/nginx/nginx.conf" do
454 source 'opt/docker-compose/app/screwdriver/etc/nginx/nginx.conf'
461 node.rm('screwdriver', 'docker-compose', 'config', 'services', 'reverseproxy')
462 # TODO: in the future.
465 node.rm('screwdriver', 'docker-compose', 'config', 'services', 'reverseproxy')
469 api_envs['URI'] = api_uri
470 api_envs['ECOSYSTEM_STORE'] = store_uri
471 api_envs['ECOSYSTEM_UI'] = ui_uri
473 ui_envs['ECOSYSTEM_API'] = api_uri
474 ui_envs['ECOSYSTEM_STORE'] = store_uri
476 store_envs['URI'] = store_uri
477 store_envs['ECOSYSTEM_UI'] = ui_uri
480 if node['screwdriver']['docker-compose']['import_ca']
481 node['screwdriver']['ssl_cert']['ca_names'].each {|ca_name|
482 append_ca_name(ca_name)
483 ca_cert_vol = "#{ca_cert_path(ca_name)}:/usr/share/ca-certificates/#{ca_name}.crt:ro"
484 api_vols.push(ca_cert_vol)
485 #ui_vols.push(ca_cert_vol)
487 include_recipe 'ssl_cert::ca_certs'
489 import_ca_script = '/usr/local/bin/screwdriver_import_ca'
490 template "#{bin_dir}/screwdriver_import_ca" do
491 source 'opt/docker-compose/app/screwdriver/bin/screwdriver_import_ca'
497 import_ca_script_vol = "#{bin_dir}/screwdriver_import_ca:#{import_ca_script}:ro"
498 api_vols.push(import_ca_script_vol)
499 #ui_vols.push(import_ca_script_vol)
501 api_command = config_srvs['api']['command']
502 override_config_srvs['api']['command'] \
503 = "/bin/sh -c \"#{import_ca_script} && #{api_command}\""
510 local_yaml_file = "#{config_dir}/#{srv}-local.yaml"
515 srv_config = override_api_config
517 srv_vols = store_vols
518 srv_config = override_store_config
521 template local_yaml_file do
522 source "opt/docker-compose/app/screwdriver/config/#{srv}-local.yaml"
527 # prevent Chef from logging password attribute value.
533 srv_vols.push("#{local_yaml_file}:/config/local.yaml:ro")
536 # merge environment hash and reset volumes array.
537 force_override_config_srvs['api']['environment'] = api_envs unless api_envs.empty?
538 override_config_srvs['api']['volumes'] = api_vols unless api_vols.empty?
539 force_override_config_srvs['ui']['environment'] = ui_envs unless ui_envs.empty?
540 override_config_srvs['ui']['volumes'] = ui_vols unless ui_vols.empty?
541 force_override_config_srvs['store']['environment'] = store_envs unless store_envs.empty?
542 override_config_srvs['store']['volumes'] = store_vols unless store_vols.empty?
543 if db_dialect != 'sqlite'
544 force_override_config_srvs['db']['environment'] = db_envs unless db_envs.empty?
545 override_config_srvs['db']['volumes'] = db_vols unless db_vols.empty?
547 if executor_queue_enabled
548 force_override_config_srvs['queue']['environment'] = queue_envs unless queue_envs.empty?
549 override_config_srvs['queue']['volumes'] = queue_vols unless queue_vols.empty?
551 if !store_backend.nil? && !store_backend.empty?
552 force_override_config_srvs['screwdriver.s3']['environment'] = s3_envs unless s3_envs.empty?
553 override_config_srvs['screwdriver.s3']['volumes'] = s3_vols unless s3_vols.empty?
557 source 'opt/docker-compose/app/screwdriver/.env'
562 # prevent Chef from logging password attribute value.
565 cookie_password: cookie_password,
567 hashing_password: hashing_password,
568 db_username: db_username,
569 db_password: db_password,
570 db_root_password: db_root_password,
571 s3_access_key_id: s3_access_key_id,
572 s3_access_key_secret: s3_access_key_secret,
574 # JWT keys setting -> /config/local.yaml
575 #jwt_private_key: jwt_private_key,
576 #jwt_public_key: jwt_public_key,
577 # SCM secrets setting -> /config/local.yaml
578 #oauth_client_id: oauth_client_id,
579 #oauth_client_secret: oauth_client_secret,
580 #webhook_github_secret: webhook_github_secret
584 template config_file do
585 source 'opt/docker-compose/app/screwdriver/docker-compose.yml'
591 log 'screwdriver docker-compose post install message' do
593 Note: You must execute the following command manually.
597 $ sudo docker-compose up -d
599 $ sudo docker-compose down