OSDN Git Service

adds MySQL support.
[metasearch/grid-chef-repo.git] / cookbooks / screwdriver / README.md
1 screwdriver Cookbook
2 ====================
3
4 This cookbook sets up a Screwdriver CI/CD service by Docker Compose.
5
6 ## Contents
7
8 - [Requirements](#requirements)
9     - [platforms](#platforms)
10     - [packages](#packages)
11     - [cookbooks](#cookbooks)
12 - [Attributes](#attributes)
13 - [Usage](#usage)
14     - [Recipes](#recipes)
15         - [screwdriver::default](#screwdriverdefault)
16         - [screwdriver::docker-compose](#screwdriverdocker-compose)
17     - [Role Examples](#role-examples)
18     - [SSL server keys and certificates management by ssl_cert cookbook](#ssl-server-keys-and-certificates-management-by-ssl_cert-cookbook)
19     - [JWT private and public keys management by Chef Vault](#jwt-private-and-public-keys-management-by-chef-vault)
20     - [Cookie password management by Chef Vault](#cookie-password-management-by-chef-vault)
21     - [Secrets encryption password management by Chef Vault](#secrets-encryption-password-management-by-chef-vault)
22     - [Database username management (for MySQL, PostgreSQL,...) by Chef Vault](#database-username-management-for-mysql-postgresql-by-chef-vault)
23     - [Database password management (for MySQL, PostgreSQL,...) by Chef Vault](#database-password-management-for-mysql-postgresql-by-chef-vault)
24     - [Database root password management (for MySQL, PostgreSQL,...) by Chef Vault](#database-root-password-management-for-mysql-postgresql-by-chef-vault)
25     - [OAuth client ID, secret and GitHub webhook secret management by Chef Vault](#oauth-client-id-secret-and-github-webhook-secret-management-by-chef-vault)
26     - [Note](#note)
27         - [Database Initialization](#database-initialization)
28 - [License and Authors](#license-and-authors)
29
30 ## Requirements
31
32 ### platforms
33 - Debian >= 9.0
34 - Ubuntu >= 14.04
35 - CentOS, RHEL >= 7.3
36
37 ### packages
38 - none.
39
40 ### cookbooks
41 - `docker-grid`
42 - `ssl_cert`
43
44 ## Attributes
45
46 |Key|Type|Description, example|Default|
47 |:--|:--|:--|:--|
48 |`['screwdriver']['with_ssl_cert_cookbook']`|Boolean|See `attributes/default.rb`|`false`|
49 |`['screwdriver']['ssl_cert']['ca_names']`|Array|Internal CA names that are imported by the ssl_cert cookbook.|`[]`|
50 |`['screwdriver']['ssl_cert']['common_name']`|String|Server common name for TLS|`node['fqdn']`|
51 |`['screwdriver']['jwt_private_key_vault_item']`|Hash|Optional, Sets a JWT private key from Chef Vault. See `attributes/default.rb`|`{}`|
52 |`['screwdriver']['jwt_public_key_vault_item']`|Hash|Optional, Sets a JWT public key from Chef Vault. See `attributes/default.rb`|`{}`|
53 |`['screwdriver']['cookie_password_vault_item']`|Hash|Optional, Sets a session cookie password from Chef Vault. See `attributes/default.rb`|`{}`|
54 |`['screwdriver']['password_vault_item']`|Hash|Optional, Sets a password for secrets encryption from Chef Vault. See `attributes/default.rb`|`{}`|
55 |`['screwdriver']['db_username_vault_item']`|Hash|Optional, Sets a database username from Chef Vault. See `attributes/default.rb`|`{}`|
56 |`['screwdriver']['db_password_vault_item']`|Hash|Optional, Sets a database password from Chef Vault. See `attributes/default.rb`|`{}`|
57 |`['screwdriver']['db_root_password_vault_item']`|Hash|Optional, Sets a database password for the root user from Chef Vault. See `attributes/default.rb`|`{}`|
58 |`['screwdriver']['ui']['tls_setup_mode']`|String|`'reverseproxy'` only. Note: [_Add TLS support to UI docker container #377_](https://github.com/screwdriver-cd/screwdriver/issues/377)|`'reverseproxy'`|
59 |`['screwdriver']['api']['config']`|Hash|This hash object is expanded to a `/config/local.yaml` file in the API Docker container.|See `attributes/default.rb`|
60 |`['screwdriver']['api']['scms_vault_items']`|Hash|This hash contains Chef Vault item definitions of SCM's secrets.|See `attributes/default.rb`|
61 |`['screwdriver']['store']['config']`|Hash|This hash object is expanded to a `/config/local.yaml` file in the Store Docker container.|See `attributes/default.rb`|
62 |`['screwdriver']['docker-compose']['import_ca']`|Boolean|whether import internal CA certificates or not.|`false`|
63 |`['screwdriver']['docker-compose']['app_dir']`|String|Path string.|`"#{node['docker-grid']['compose']['app_dir']}/screwdriver"`|
64 |`['screwdriver']['docker-compose']['bin_dir']`|String|Path string.|`"#{node['screwdriver']['docker-compose']['app_dir']}/bin"`|
65 |`['screwdriver']['docker-compose']['config_dir']`|String|Path string.|`"#{node['screwdriver']['docker-compose']['app_dir']}/config"`|
66 |`['screwdriver']['docker-compose']['data_dir']`|String|Path string.|`"#{node['screwdriver']['docker-compose']['app_dir']}/data"`|
67 |`['screwdriver']['docker-compose']['etc_dir']`|String|Path string.|`"#{node['screwdriver']['docker-compose']['app_dir']}/etc"`|
68 |`['screwdriver']['docker-compose']['jwt_private_key_reset']`|Boolean|Only available if the JWT key pair is automatically generated by Chef.|`false`|
69 |`['screwdriver']['docker-compose']['jwt_private_key_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['jwt_private_key_vault_item']`. Optional, Sets a JWT private key from Chef Vault. See `attributes/default.rb`|`{}`|
70 |`['screwdriver']['docker-compose']['jwt_public_key_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['jwt_public_key_vault_item']`. Optional, Sets a JWT public key from Chef Vault. See `attributes/default.rb`|`{}`|
71 |`['screwdriver']['docker-compose']['cookie_password_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['cookie_password_vault_item']`. Optional, Sets a session cookie password from Chef Vault. See `attributes/default.rb`|`{}`|
72 |`['screwdriver']['docker-compose']['password_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['password_vault_item']`. Optional, Sets a password for secrets encryption from Chef Vault. See `attributes/default.rb`|`{}`|
73 |`['screwdriver']['docker-compose']['oauth_client_id_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['api']['scms_vault_items']`. Required, Sets a OAuth client ID for SCM from Chef Vault. See `attributes/default.rb`|`{}`|
74 |`['screwdriver']['docker-compose']['oauth_client_secret_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['api']['scms_vault_items']`. Required, Sets a OAuth secret for SCM from Chef Vault. See `attributes/default.rb`|`{}`|
75 |`['screwdriver']['docker-compose']['webhook_github_secret_vault_item']`|Hash|**DEPRECATED**: use `['screwdriver']['api']['scms_vault_items']`. Required for GitHub, Sets a secret for GitHub webhook from Chef Vault. See `attributes/default.rb`|`{}`|
76 |`['screwdriver']['docker-compose']['config']`|Hash|`docker-compose.yml` configurations.|See `attributes/default.rb`|
77
78 ## Usage
79
80 ### Recipes
81
82 #### screwdriver::default
83
84 This recipe does nothing.
85
86 #### screwdriver::docker-compose
87
88 This recipe generates JWT key pair and a `docker-compose.yml` file for the Screwdriver CI/CD service.
89
90 ### Role Examples
91
92 - `roles/screwdriver.rb`
93
94 ```ruby
95 name 'screwdriver'
96 description 'screwdriver'
97
98 ui_port     = '9000'
99 api_port    = '9001'
100 store_port  = '9002'
101
102 run_list(
103   'role[docker]',
104   'recipe[screwdriver::docker-compose]',
105 )
106
107 override_attributes(
108   'screwdriver' => {
109     'api' => {
110       'config' => {
111         'executor' => {
112           'plugin' => 'docker',
113           'docker' => {
114             'options' => {
115               'docker' => {
116                 'socketPath' => '/var/run/docker.sock',
117               },
118               'launchVersion' => 'stable',
119             },
120           },
121         },
122         'scms' => {
123           'github.com' => {
124             'plugin' => 'github',
125             'config' => {
126               # OAuth Callback URL: "http://#{node['fqdn']}:9001/v4/auth/login/web"
127               'username' => 'ci-tool',
128               'email' => 'citool@mail.example.com',
129               'privateRepo' => false,
130             },
131           },
132         },
133       },
134       'scms_vault_items' => {
135         'github.com' => {
136           'oauthClientId' => {
137             'vault' => 'screwdriver',
138             'name' => 'github',
139             'env_context' => false,
140             'key' => 'oauthClientId',  # real hash path: "/oauthClientId"
141           },
142           'oauthClientSecret' => {
143             'vault' => 'screwdriver',
144             'name' => 'github',
145             'env_context' => false,
146             'key' => 'oauthClientSecret',  # real hash path: "/oauthClientSecret"
147           },
148           'secret' => {
149             'vault' => 'screwdriver',
150             'name' => 'github',
151             'env_context' => false,
152             'key' => 'secret',  # real hash path: "/secret"
153           },
154         },
155       },
156     },
157     'docker-compose' => {
158       'config' => {
159         'services' => {
160           'api' => {
161             'ports' => [
162               "#{api_port}:80",
163             ],
164             'environment' => {
165               'NODE_TLS_REJECT_UNAUTHORIZED' => '0',  # for self-signed cetificates
166               # The following variables will be set by the screwdriver::docker-compose recipe automatically.
167               #'ECOSYSTEM_UI' => "http://#{node['fqdn']}:#{ui_port}",
168               #'ECOSYSTEM_STORE' => "http://#{node['fqdn']}:#{store_port}",
169             },
170           },
171           'ui' => {
172             'ports' => [
173               "#{ui_port}:80",
174             ],
175             'environment' => {
176               # These variables will be set by the screwdriver::docker-compose recipe automatically.
177               #'ECOSYSTEM_API' => "http://#{node['fqdn']}:#{api_port}",
178               #'ECOSYSTEM_STORE' => "http://#{node['fqdn']}:#{store_port}",
179             },
180           },
181           'store' => {
182             'ports' => [
183               "#{store_port}:80",
184             ],
185             'environment' => {
186               # This variable will be set by the screwdriver::docker-compose recipe automatically.
187               #'ECOSYSTEM_UI' => "http://#{node['fqdn']}:#{ui_port}",
188             },
189           },
190         },
191       },
192     },
193   },
194 )
195 ```
196
197 - `roles/screwdriver-with-ssl.rb`
198
199 ```ruby
200 name 'screwdriver-with-ssl'
201 description 'screwdriver with SSL'
202
203 cn = 'screwdriver.io.example.com'
204 ui_port     = '9000'
205 api_port    = '9001'
206 store_port  = '9002'
207
208 run_list(
209   'role[docker]',
210   'recipe[screwdriver::docker-compose]',
211 )
212
213 override_attributes(
214   'ssl_cert' => {
215     #'common_names' => [
216     #  cn,  # screwdriver cookbook < 0.2.2
217     #],
218   },
219   'screwdriver' => {
220     'with_ssl_cert_cookbook' => true,
221     'ssl_cert' => {
222       'common_name' => cn,
223     },
224     'api' => {
225       'config' => {
226         'executor' => {
227           'plugin' => 'docker',
228           'docker' => {
229             'options' => {
230               'docker' => {
231                 'socketPath' => '/var/run/docker.sock',
232               },
233               'launchVersion' => 'stable',
234             },
235           },
236         },
237         'scms' => {
238           'github.com' => {
239             'plugin' => 'github',
240             'config' => {
241               # OAuth Callback URL: "http://#{node['fqdn']}:9001/v4/auth/login/web"
242               'username' => 'ci-tool',
243               'email' => 'citool@mail.example.com',
244               'privateRepo' => false,
245             },
246           },
247         },
248       },
249       'scms_vault_items' => {
250         'github.com' => {
251           'oauthClientId' => {
252             'vault' => 'screwdriver',
253             'name' => 'github',
254             'env_context' => false,
255             'key' => 'oauthClientId',  # real hash path: "/oauthClientId"
256           },
257           'oauthClientSecret' => {
258             'vault' => 'screwdriver',
259             'name' => 'github',
260             'env_context' => false,
261             'key' => 'oauthClientSecret',  # real hash path: "/oauthClientSecret"
262           },
263           'secret' => {
264             'vault' => 'screwdriver',
265             'name' => 'github',
266             'env_context' => false,
267             'key' => 'secret',  # real hash path: "/secret"
268           },
269         },
270       },
271     },
272     'docker-compose' => {
273       'config' => {
274         'services' => {
275           'reverseproxy' => {
276             'ports' => [
277               "#{ui_port}:9000",
278             ],
279             'environment' => {
280             },
281           },
282           'api' => {
283             'ports' => [
284               "#{api_port}:80",
285             ],
286             'environment' => {
287               'NODE_TLS_REJECT_UNAUTHORIZED' => '0',  # for self-signed cetificates
288               # The following variables will be set by the screwdriver::docker-compose recipe automatically.
289               #'ECOSYSTEM_UI' => "http://#{node['fqdn']}:#{ui_port}",
290               #'ECOSYSTEM_STORE' => "http://#{node['fqdn']}:#{store_port}",
291             },
292           },
293           'ui' => {
294             #'ports' => [
295             #  "#{ui_port}:80",
296             #],
297             'environment' => {
298               # These variables will be set by the screwdriver::docker-compose recipe automatically.
299               #'ECOSYSTEM_API' => "http://#{node['fqdn']}:#{api_port}",
300               #'ECOSYSTEM_STORE' => "http://#{node['fqdn']}:#{store_port}",
301             },
302           },
303           'store' => {
304             'ports' => [
305               "#{store_port}:80",
306             ],
307             'environment' => {
308               # These variables will be set by the screwdriver::docker-compose recipe automatically.
309               #'ECOSYSTEM_UI' => "http://#{node['fqdn']}:#{ui_port}",
310             },
311           },
312         },
313       },
314     },
315   },
316 )
317 ```
318
319 ### SSL server keys and certificates management by ssl_cert cookbook
320
321 - create vault items.
322
323 ```text
324 $ ruby -rjson -e 'puts JSON.generate({"private" => File.read("screwdriver.io.example.com.prod.key")})' \
325 > > ~/sec/tmp/screwdriver.io.example.com.prod.key.json
326
327 $ ruby -rjson -e 'puts JSON.generate({"public" => File.read("screwdriver.io.example.com.prod.crt")})' \
328 > > ~/sec/tmp/screwdriver.io.example.com.prod.crt.json
329
330 $ cd $CHEF_REPO_PATH
331
332 $ knife vault create ssl_server_keys screwdriver.io.example.com.prod \
333 > --json ~/sec/tmp/screwdriver.io.example.com.prod.key.json
334
335 $ knife vault create ssl_server_certs screwdriver.io.example.com.prod \
336 > --json ~/sec/tmp/screwdriver.io.example.com.prod.crt.json
337 ```
338
339 - grant reference permission to the screwdriver host
340
341 ```text
342 $ knife vault update ssl_server_keys  screwdriver.io.example.com.prod -S 'name:screwdriver-host.example.com'
343 $ knife vault update ssl_server_certs screwdriver.io.example.com.prod -S 'name:screwdriver-host.example.com'
344 ```
345
346 - modify attributes
347
348 ```ruby
349 override_attributes(
350   'ssl_cert' => {
351     #'common_names' => [
352     #  'screwdriver.io.example.com',  # screwdriver cookbook < 0.2.2
353     #],
354   },
355   'screwdriver' => {
356     'with_ssl_cert_cookbook' => true,
357     'ssl_cert' => {
358       'common_name' => 'screwdriver.io.example.com',
359     },
360     # ...
361   },
362 )
363 ```
364
365 ### JWT private and public keys management by Chef Vault
366
367 - create vault items.
368
369 ```text
370 $ ruby -rjson -e 'puts JSON.generate({"private" => File.read("screwdriver_jwt_private.key")})' \
371 > > ~/sec/tmp/screwdriver_jwt_private.key.json
372
373 $ ruby -rjson -e 'puts JSON.generate({"public" => File.read("screwdriver_jwt_public.key")})' \
374 > > ~/sec/tmp/screwdriver_jwt_public.key.json
375
376 $ cd $CHEF_REPO_PATH
377
378 $ knife vault create screwdriver jwt_private_key \
379 > --json ~/sec/tmp/screwdriver_jwt_private.key.json
380
381 $ knife vault create screwdriver screwdriver_jwt_public \
382 > --json ~/sec/tmp/screwdriver_jwt_public.key.json
383 ```
384
385 - grant reference permission to the screwdriver host
386
387 ```text
388 $ knife vault update screwdriver jwt_private_key -S 'name:screwdriver-host.example.com'
389 $ knife vault update screwdriver jwt_public_key  -S 'name:screwdriver-host.example.com'
390 ```
391
392 - modify attributes
393
394 ```ruby
395 override_attributes(
396   'screwdriver' => {
397     # ...
398     'jwt_private_key_vault_item' => {
399       'vault' => 'screwdriver',
400       'name' => 'jwt_private_key',
401       'env_context' => false,
402       'key' => 'private',
403     },
404     'jwt_public_key_vault_item' => {
405       'vault' => 'screwdriver',
406       'name' => 'jwt_public_key',
407       'env_context' => false,
408       'key' => 'public',
409     },
410     # ...
411   },
412 )
413 ```
414
415 ### Cookie password management by Chef Vault
416
417 - create vault items.
418
419 ```text
420 # A password used for encrypting session data. Needs to be minimum 32 characters
421 $ cat ~/sec/tmp/screwdriver_cookie_password.json
422 {"password":"********************************"}
423
424 $ cd $CHEF_REPO_PATH
425 $ knife vault create screwdriver cookie_password --json ~/sec/tmp/screwdriver_cookie_password.json
426 ```
427
428 - grant reference permission to the screwdriver host
429
430 ```text
431 $ knife vault update screwdriver cookie_password -S 'name:screwdriver-host.example.com'
432 ```
433
434 - modify attributes
435
436 ```ruby
437 override_attributes(
438   'screwdriver' => {
439     # ...
440     'cookie_password_vault_item' => {
441       'vault' => 'screwdriver',
442       'name' => 'cookie_password',
443       'env_context' => false,
444       'key' => 'password',
445     },
446     # ...
447   },
448 )
449 ```
450
451 ### Secrets encryption password management by Chef Vault
452
453 - create vault items.
454
455 ```text
456 # A password used for encrypting stored secrets. Needs to be minimum 32 characters
457 $ cat ~/sec/tmp/screwdriver_password.json
458 {"password":"********************************"}
459
460 $ cd $CHEF_REPO_PATH
461 $ knife vault create screwdriver password --json ~/sec/tmp/screwdriver_password.json
462 ```
463
464 - grant reference permission to the screwdriver host
465
466 ```text
467 $ knife vault update screwdriver password -S 'name:screwdriver-host.example.com'
468 ```
469
470 - modify attributes
471
472 ```ruby
473 override_attributes(
474   'screwdriver' => {
475     # ...
476     'password_vault_item' => {
477       'vault' => 'screwdriver',
478       'name' => 'password',
479       'env_context' => false,
480       'key' => 'password',
481     },
482     # ...
483   },
484 )
485 ```
486
487 ### Database username management (for MySQL, PostgreSQL,...) by Chef Vault
488
489 - create vault items.
490
491 ```text
492 $ cat ~/sec/tmp/screwdriver_db_username.json
493 {"username":"********************************"}
494
495 $ cd $CHEF_REPO_PATH
496 $ knife vault create screwdriver db_username --json ~/sec/tmp/screwdriver_db_username.json
497 ```
498
499 - grant reference permission to the screwdriver host
500
501 ```text
502 $ knife vault update screwdriver db_username -S 'name:screwdriver-host.example.com'
503 ```
504
505 - modify attributes
506
507 ```ruby
508 override_attributes(
509   'screwdriver' => {
510     # ...
511     'db_username_vault_item' => {
512       'vault' => 'screwdriver',
513       'name' => 'db_username',
514       'env_context' => false,
515       'key' => 'username',
516     },
517     # ...
518   },
519 )
520 ```
521
522 ### Database password management (for MySQL, PostgreSQL,...) by Chef Vault
523
524 - create vault items.
525
526 ```text
527 $ cat ~/sec/tmp/screwdriver_db_password.json
528 {"password":"********************************"}
529
530 $ cd $CHEF_REPO_PATH
531 $ knife vault create screwdriver db_password --json ~/sec/tmp/screwdriver_db_password.json
532 ```
533
534 - grant reference permission to the screwdriver host
535
536 ```text
537 $ knife vault update screwdriver db_password -S 'name:screwdriver-host.example.com'
538 ```
539
540 - modify attributes
541
542 ```ruby
543 override_attributes(
544   'screwdriver' => {
545     # ...
546     'db_password_vault_item' => {
547       'vault' => 'screwdriver',
548       'name' => 'db_password',
549       'env_context' => false,
550       'key' => 'password',
551     },
552     # ...
553   },
554 )
555 ```
556
557 ### Database root password management (for MySQL, PostgreSQL,...) by Chef Vault
558
559 - create vault items.
560
561 ```text
562 $ cat ~/sec/tmp/screwdriver_db_root_password.json
563 {"password":"********************************"}
564
565 $ cd $CHEF_REPO_PATH
566 $ knife vault create screwdriver db_root_password --json ~/sec/tmp/screwdriver_db_root_password.json
567 ```
568
569 - grant reference permission to the screwdriver host
570
571 ```text
572 $ knife vault update screwdriver db_root_password -S 'name:screwdriver-host.example.com'
573 ```
574
575 - modify attributes
576
577 ```ruby
578 override_attributes(
579   'screwdriver' => {
580     # ...
581     'db_root_password_vault_item' => {
582       'vault' => 'screwdriver',
583       'name' => 'db_root_password',
584       'env_context' => false,
585       'key' => 'password',
586     },
587     # ...
588   },
589 )
590 ```
591
592 ### OAuth client ID, secret and GitHub webhook secret management by Chef Vault
593
594 - create vault items.
595
596 ```text
597 $ cat ~/sec/tmp/screwdriver_github_secrets.json
598 {
599   "oauthClientId": "***************************************************************",
600   "oauthClientSecret": "***************************************************************",
601   "secret": "**************************"
602 }
603 ```
604
605 $ cd $CHEF_REPO_PATH
606
607 ```text
608 $ knife vault create screwdriver github --json ~/sec/tmp/screwdriver_github_secrets.json
609 ```
610
611 - grant reference permission to the screwdriver host
612
613 ```text
614 $ knife vault update screwdriver github -S 'name:screwdriver-host.example.com'
615 ```
616
617 - modify attributes
618
619 ```ruby
620 override_attributes(
621   'screwdriver' => {
622     # ...
623     'api' => {
624       # ...
625       'scms_vault_items' => {
626         'github.com' => {
627           'oauthClientId' => {
628             'vault' => 'screwdriver',
629             'name' => 'github',
630             'env_context' => false,
631             'key' => 'oauthClientId',  # real hash path: "/oauthClientId"
632           },
633           'oauthClientSecret' => {
634             'vault' => 'screwdriver',
635             'name' => 'github',
636             'env_context' => false,
637             'key' => 'oauthClientSecret',  # real hash path: "/oauthClientSecret"
638           },
639           'secret' => {
640             'vault' => 'screwdriver',
641             'name' => 'github',
642             'env_context' => false,
643             'key' => 'secret',  # real hash path: "/secret"
644           },
645         },
646       },
647     },
648     # ...
649   },
650 )
651 ```
652
653 ### Note
654
655 #### Database Initialization
656
657 If you use database other than sqlite, its database initialization will takes a few tens of seconds.
658 You should run a database container only at the beginning and then start the others. 
659 ```
660 $ sudo docker-compose up -d db
661 ...
662 Creating network "screwdriver_default" with the default driver
663 Creating screwdriver_db_1 ... done
664
665 $ sudo docker-compose up -d
666 screwdriver_db_1 is up-to-date
667 Creating screwdriver_api_1   ... done
668 Creating screwdriver_ui_1    ... done
669 Creating screwdriver_store_1 ... done
670 ```
671
672 ## License and Authors
673
674 - Author:: whitestar at osdn.jp
675
676 ```text
677 Copyright 2017, whitestar
678
679 Licensed under the Apache License, Version 2.0 (the "License");
680 you may not use this file except in compliance with the License.
681 You may obtain a copy of the License at
682
683     http://www.apache.org/licenses/LICENSE-2.0
684
685 Unless required by applicable law or agreed to in writing, software
686 distributed under the License is distributed on an "AS IS" BASIS,
687 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
688 See the License for the specific language governing permissions and
689 limitations under the License.
690 ```