kontena-cli 0.8.4 → 0.9.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/kontena +4 -0
  4. data/lib/kontena/cli/app_command.rb +2 -0
  5. data/lib/kontena/cli/apps/build_command.rb +26 -0
  6. data/lib/kontena/cli/apps/common.rb +41 -12
  7. data/lib/kontena/cli/apps/deploy_command.rb +31 -13
  8. data/lib/kontena/cli/apps/docker_helper.rb +34 -0
  9. data/lib/kontena/cli/apps/init_command.rb +130 -10
  10. data/lib/kontena/cli/apps/list_command.rb +4 -2
  11. data/lib/kontena/cli/apps/logs_command.rb +4 -2
  12. data/lib/kontena/cli/apps/remove_command.rb +4 -2
  13. data/lib/kontena/cli/apps/start_command.rb +4 -2
  14. data/lib/kontena/cli/apps/stop_command.rb +4 -2
  15. data/lib/kontena/cli/common.rb +3 -3
  16. data/lib/kontena/cli/container_command.rb +3 -0
  17. data/lib/kontena/cli/containers/inspect_command.rb +16 -0
  18. data/lib/kontena/cli/deploy_command.rb +3 -0
  19. data/lib/kontena/cli/etcd/get_command.rb +21 -0
  20. data/lib/kontena/cli/etcd/list_command.rb +26 -0
  21. data/lib/kontena/cli/etcd/mkdir_command.rb +17 -0
  22. data/lib/kontena/cli/etcd/remove_command.rb +21 -0
  23. data/lib/kontena/cli/etcd/set_command.rb +18 -0
  24. data/lib/kontena/cli/etcd_command.rb +17 -0
  25. data/lib/kontena/cli/grid_command.rb +2 -0
  26. data/lib/kontena/cli/grids/logs_command.rb +71 -0
  27. data/lib/kontena/cli/master/aws/create_command.rb +33 -0
  28. data/lib/kontena/cli/master/aws_command.rb +8 -0
  29. data/lib/kontena/cli/master/azure/create_command.rb +33 -0
  30. data/lib/kontena/cli/master/azure_command.rb +13 -0
  31. data/lib/kontena/cli/master/digital_ocean/create_command.rb +30 -0
  32. data/lib/kontena/cli/master/digital_ocean_command.rb +13 -0
  33. data/lib/kontena/cli/master/vagrant/create_command.rb +19 -0
  34. data/lib/kontena/cli/master/vagrant/restart_command.rb +20 -0
  35. data/lib/kontena/cli/master/vagrant/ssh_command.rb +15 -0
  36. data/lib/kontena/cli/master/vagrant/start_command.rb +20 -0
  37. data/lib/kontena/cli/master/vagrant/stop_command.rb +20 -0
  38. data/lib/kontena/cli/master/vagrant/terminate_command.rb +13 -0
  39. data/lib/kontena/cli/master/vagrant_command.rb +23 -0
  40. data/lib/kontena/cli/master_command.rb +15 -0
  41. data/lib/kontena/cli/node_command.rb +4 -0
  42. data/lib/kontena/cli/nodes/aws/create_command.rb +39 -0
  43. data/lib/kontena/cli/nodes/aws/restart_command.rb +28 -0
  44. data/lib/kontena/cli/nodes/aws/terminate_command.rb +20 -0
  45. data/lib/kontena/cli/nodes/aws_command.rb +15 -0
  46. data/lib/kontena/cli/nodes/azure/create_command.rb +39 -0
  47. data/lib/kontena/cli/nodes/azure/restart_command.rb +31 -0
  48. data/lib/kontena/cli/nodes/azure/terminate_command.rb +20 -0
  49. data/lib/kontena/cli/nodes/azure_command.rb +15 -0
  50. data/lib/kontena/cli/nodes/digital_ocean/create_command.rb +1 -1
  51. data/lib/kontena/cli/nodes/vagrant/create_command.rb +1 -1
  52. data/lib/kontena/cli/service_command.rb +4 -0
  53. data/lib/kontena/cli/services/add_env_command.rb +18 -0
  54. data/lib/kontena/cli/services/create_command.rb +8 -0
  55. data/lib/kontena/cli/services/remove_env_command.rb +17 -0
  56. data/lib/kontena/cli/services/services_helper.rb +20 -1
  57. data/lib/kontena/cli/services/update_command.rb +10 -0
  58. data/lib/kontena/client.rb +22 -1
  59. data/lib/kontena/machine/aws.rb +13 -0
  60. data/lib/kontena/machine/aws/cloudinit.yml +66 -0
  61. data/lib/kontena/machine/aws/cloudinit_master.yml +105 -0
  62. data/lib/kontena/machine/aws/master_provisioner.rb +161 -0
  63. data/lib/kontena/machine/aws/node_destroyer.rb +39 -0
  64. data/lib/kontena/machine/aws/node_provisioner.rb +168 -0
  65. data/lib/kontena/machine/azure.rb +13 -0
  66. data/lib/kontena/machine/azure/cloudinit.yml +59 -0
  67. data/lib/kontena/machine/azure/cloudinit_master.yml +105 -0
  68. data/lib/kontena/machine/azure/logger.rb +27 -0
  69. data/lib/kontena/machine/azure/master_provisioner.rb +126 -0
  70. data/lib/kontena/machine/azure/node_destroyer.rb +53 -0
  71. data/lib/kontena/machine/azure/node_provisioner.rb +128 -0
  72. data/lib/kontena/machine/digital_ocean.rb +1 -0
  73. data/lib/kontena/machine/digital_ocean/cloudinit.yml +1 -0
  74. data/lib/kontena/machine/digital_ocean/cloudinit_master.yml +105 -0
  75. data/lib/kontena/machine/digital_ocean/master_provisioner.rb +94 -0
  76. data/lib/kontena/machine/digital_ocean/node_provisioner.rb +8 -1
  77. data/lib/kontena/machine/vagrant.rb +2 -0
  78. data/lib/kontena/machine/vagrant/Vagrantfile.master.rb.erb +101 -0
  79. data/lib/kontena/machine/vagrant/{Vagrantfile.coreos.rb.erb → Vagrantfile.node.rb.erb} +0 -0
  80. data/lib/kontena/machine/vagrant/cloudinit.yml +2 -1
  81. data/lib/kontena/machine/vagrant/master_destroyer.rb +37 -0
  82. data/lib/kontena/machine/vagrant/master_provisioner.rb +75 -0
  83. data/lib/kontena/machine/vagrant/node_destroyer.rb +4 -0
  84. data/lib/kontena/machine/vagrant/node_provisioner.rb +1 -1
  85. data/lib/kontena/scripts/completer +29 -3
  86. data/spec/kontena/cli/app/common_spec.rb +61 -0
  87. data/spec/kontena/cli/app/deploy_command_spec.rb +25 -6
  88. data/spec/kontena/cli/app/docker_helper_spec.rb +32 -0
  89. data/spec/kontena/cli/common_spec.rb +53 -0
  90. metadata +61 -3
@@ -0,0 +1,13 @@
1
+
2
+ module Kontena::Cli::Master
3
+
4
+ require_relative 'azure/create_command'
5
+
6
+ class AzureCommand < Clamp::Command
7
+
8
+ subcommand "create", "Create a new Azure master", Azure::CreateCommand
9
+
10
+ def execute
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ module Kontena::Cli::Master::DigitalOcean
2
+ class CreateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ option "--token", "TOKEN", "DigitalOcean API token", required: true
6
+ option "--ssh-key", "SSH_KEY", "Path to ssh public key", required: true
7
+ option "--ssl-cert", "SSL CERT", "SSL certificate file"
8
+ option "--size", "SIZE", "Droplet size", default: '1gb'
9
+ option "--region", "REGION", "Region", default: 'ams2'
10
+ option "--version", "VERSION", "Define installed Kontena version", default: 'latest'
11
+ option "--auth-provider-url", "AUTH_PROVIDER_URL", "Define authentication provider url"
12
+
13
+
14
+ def execute
15
+
16
+ require 'kontena/machine/digital_ocean'
17
+
18
+ provisioner = Kontena::Machine::DigitalOcean::MasterProvisioner.new(token)
19
+ provisioner.run!(
20
+ ssh_key: ssh_key,
21
+ ssl_cert: ssl_cert,
22
+ size: size,
23
+ region: region,
24
+ version: version,
25
+ auth_server: auth_provider_url,
26
+ )
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module Kontena::Cli::Master
3
+
4
+ require_relative 'digital_ocean/create_command'
5
+
6
+ class DigitalOceanCommand < Clamp::Command
7
+
8
+ subcommand "create", "Create a new DigitalOcean master", DigitalOcean::CreateCommand
9
+
10
+ def execute
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Kontena::Cli::Master::Vagrant
2
+ class CreateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ option "--memory", "MEMORY", "How much memory node has", default: '512'
6
+ option "--version", "VERSION", "Define installed Kontena version", default: 'latest'
7
+ option "--auth-provider-url", "AUTH_PROVIDER_URL", "Define authentication provider url"
8
+
9
+ def execute
10
+ require 'kontena/machine/vagrant'
11
+ provisioner = Kontena::Machine::Vagrant::MasterProvisioner.new
12
+ provisioner.run!(
13
+ memory: memory,
14
+ version: version,
15
+ auth_server: auth_provider_url
16
+ )
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Kontena::Cli::Master::Vagrant
2
+ class RestartCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ def execute
6
+ require 'kontena/machine/vagrant'
7
+ vagrant_path = "#{Dir.home}/.kontena/vagrant_master"
8
+ abort("Cannot find Vagrant kontena-master".colorize(:red)) unless Dir.exist?(vagrant_path)
9
+ Dir.chdir(vagrant_path) do
10
+ ShellSpinner "Restarting Vagrant kontena-master " do
11
+ Open3.popen2('vagrant reload') do |stdin, output, wait|
12
+ while o = output.gets
13
+ print o if ENV['DEBUG']
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module Kontena::Cli::Master::Vagrant
2
+ class SshCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ def execute
6
+ require 'kontena/machine/vagrant'
7
+ vagrant_path = "#{Dir.home}/.kontena/vagrant_master"
8
+ abort("Cannot find Vagrant kontena-master".colorize(:red)) unless Dir.exist?(vagrant_path)
9
+
10
+ Dir.chdir(vagrant_path) do
11
+ system('vagrant ssh')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Kontena::Cli::Master::Vagrant
2
+ class StartCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ def execute
6
+ require 'kontena/machine/vagrant'
7
+ vagrant_path = "#{Dir.home}/.kontena/vagrant_master"
8
+ abort("Cannot find Vagrant node #{name}".colorize(:red)) unless Dir.exist?(vagrant_path)
9
+ Dir.chdir(vagrant_path) do
10
+ ShellSpinner "Starting Vagrant machine #{name.colorize(:cyan)} " do
11
+ Open3.popen2('vagrant up') do |stdin, output, wait|
12
+ while o = output.gets
13
+ print o if ENV['DEBUG']
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Kontena::Cli::Master::Vagrant
2
+ class StopCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ def execute
6
+ require 'kontena/machine/vagrant'
7
+ vagrant_path = "#{Dir.home}/.kontena/vagrant_master"
8
+ abort("Cannot find Vagrant kontena-master".colorize(:red)) unless Dir.exist?(vagrant_path)
9
+ Dir.chdir(vagrant_path) do
10
+ ShellSpinner "Stopping Vagrant kontena-master " do
11
+ Open3.popen2('vagrant halt') do |stdin, output, wait|
12
+ while o = output.gets
13
+ print o if ENV['DEBUG']
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Kontena::Cli::Master::Vagrant
2
+ class TerminateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ def execute
6
+ require_api_url
7
+
8
+ require 'kontena/machine/vagrant'
9
+ destroyer = Kontena::Machine::Vagrant::MasterDestroyer.new(client(require_token))
10
+ destroyer.run!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+
2
+ module Kontena::Cli::Master
3
+
4
+ require_relative 'vagrant/create_command'
5
+ require_relative 'vagrant/start_command'
6
+ require_relative 'vagrant/stop_command'
7
+ require_relative 'vagrant/restart_command'
8
+ require_relative 'vagrant/ssh_command'
9
+ require_relative 'vagrant/terminate_command'
10
+
11
+ class VagrantCommand < Clamp::Command
12
+
13
+ subcommand "create", "Create a new Vagrant master", Vagrant::CreateCommand
14
+ subcommand "ssh", "SSH into Vagrant master", Vagrant::SshCommand
15
+ subcommand "start", "Start Vagrant master", Vagrant::StartCommand
16
+ subcommand "stop", "Stop Vagrant master", Vagrant::StopCommand
17
+ subcommand "restart", "Restart Vagrant master", Vagrant::RestartCommand
18
+ subcommand "terminate", "Terminate Vagrant master", Vagrant::TerminateCommand
19
+
20
+ def execute
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'master/vagrant_command'
2
+ require_relative 'master/aws_command'
3
+ require_relative 'master/digital_ocean_command'
4
+ require_relative 'master/azure_command'
5
+
6
+ class Kontena::Cli::MasterCommand < Clamp::Command
7
+
8
+ subcommand "vagrant", "Vagrant specific commands", Kontena::Cli::Master::VagrantCommand
9
+ subcommand "aws", "AWS specific commands", Kontena::Cli::Master::AwsCommand
10
+ subcommand "digitalocean", "DigitalOcean specific commands", Kontena::Cli::Master::DigitalOceanCommand
11
+ subcommand "azure", "Azure specific commands", Kontena::Cli::Master::AzureCommand
12
+
13
+ def execute
14
+ end
15
+ end
@@ -5,6 +5,8 @@ require_relative 'nodes/update_command'
5
5
 
6
6
  require_relative 'nodes/vagrant_command'
7
7
  require_relative 'nodes/digital_ocean_command'
8
+ require_relative 'nodes/aws_command'
9
+ require_relative 'nodes/azure_command'
8
10
 
9
11
  class Kontena::Cli::NodeCommand < Clamp::Command
10
12
 
@@ -15,6 +17,8 @@ class Kontena::Cli::NodeCommand < Clamp::Command
15
17
 
16
18
  subcommand "vagrant", "Vagrant specific commands", Kontena::Cli::Nodes::VagrantCommand
17
19
  subcommand "digitalocean", "DigitalOcean specific commands", Kontena::Cli::Nodes::DigitalOceanCommand
20
+ subcommand "aws", "AWS specific commands", Kontena::Cli::Nodes::AwsCommand
21
+ subcommand "azure", "Azure specific commands", Kontena::Cli::Nodes::AzureCommand
18
22
 
19
23
  def execute
20
24
  end
@@ -0,0 +1,39 @@
1
+ module Kontena::Cli::Nodes::Aws
2
+ class CreateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ parameter "[NAME]", "Node name"
6
+ option "--access-key", "ACCESS_KEY", "AWS access key ID", required: true
7
+ option "--secret-key", "SECRET_KEY", "AWS secret key", required: true
8
+ option "--region", "REGION", "EC2 Region", default: 'eu-west-1'
9
+ option "--zone", "ZONE", "EC2 Availability Zone", default: 'a'
10
+ option "--vpc-id", "VPC ID", "Virtual Private Cloud (VPC) ID"
11
+ option "--subnet-id", "SUBNET ID", "VPC option to specify subnet to launch instance into"
12
+ option "--key-pair", "KEY_PAIR", "EC2 Key Pair", required: true
13
+ option "--type", "SIZE", "Instance type", default: 't2.small'
14
+ option "--storage", "STORAGE", "Storage size (GiB)", default: '30'
15
+ option "--version", "VERSION", "Define installed Kontena version", default: 'latest'
16
+
17
+ def execute
18
+ require_api_url
19
+ require_current_grid
20
+
21
+ require 'kontena/machine/aws'
22
+ grid = client(require_token).get("grids/#{current_grid}")
23
+ provisioner = Kontena::Machine::Aws::NodeProvisioner.new(client(require_token), access_key, secret_key, region)
24
+ provisioner.run!(
25
+ master_uri: api_url,
26
+ grid_token: grid['token'],
27
+ grid: current_grid,
28
+ name: name,
29
+ type: type,
30
+ vpc: vpc_id,
31
+ zone: zone,
32
+ subnet: subnet_id,
33
+ storage: storage,
34
+ version: version,
35
+ key_pair: key_pair
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ module Kontena::Cli::Nodes::Aws
2
+ class RestartCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ parameter "NAME", "Node name"
6
+ option "--access-key", "ACCESS_KEY", "AWS access key ID", required: true
7
+ option "--secret-key", "SECRET_KEY", "AWS secret key", required: true
8
+ option "--region", "REGION", "EC2 Region", required: true
9
+
10
+ def execute
11
+ require_api_url
12
+ require_current_grid
13
+
14
+ require 'kontena/machine/aws'
15
+
16
+ client = Fog::Compute.new(:provider => 'AWS', :aws_access_key_id => access_key, :aws_secret_access_key => secret_key, :region => region)
17
+ instance = client.servers.all({'tag:kontena_name' => name}).first
18
+ if instance
19
+ instance.reboot
20
+ ShellSpinner "Restarting AWS instance #{name.colorize(:cyan)} " do
21
+ instance.wait_for { ready? }
22
+ end
23
+ else
24
+ abort "Cannot find instance #{name.colorize(:cyan)} in AWS"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ module Kontena::Cli::Nodes::Aws
2
+ class TerminateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ parameter "NAME", "Node name"
6
+ option "--access-key", "ACCESS_KEY", "AWS access key ID", required: true
7
+ option "--secret-key", "SECRET_KEY", "AWS secret key", required: true
8
+ option "--region", "REGION", "EC2 Region", required: true
9
+
10
+ def execute
11
+ require_api_url
12
+ require_current_grid
13
+
14
+ require 'kontena/machine/aws'
15
+ grid = client(require_token).get("grids/#{current_grid}")
16
+ destroyer = Kontena::Machine::Aws::NodeDestroyer.new(client(require_token), access_key, secret_key, region)
17
+ destroyer.run!(grid, name)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'aws/create_command'
2
+ require_relative 'aws/terminate_command'
3
+ require_relative 'aws/restart_command'
4
+
5
+ module Kontena::Cli::Nodes
6
+ class AwsCommand < Clamp::Command
7
+
8
+ subcommand "create", "Create a new AWS node", Aws::CreateCommand
9
+ subcommand "terminate", "Terminate AWS node", Aws::TerminateCommand
10
+ subcommand "restart", "Restart AWS node", Aws::RestartCommand
11
+
12
+ def execute
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ module Kontena::Cli::Nodes::Azure
2
+ class CreateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ option "--subscription-id", "SUBSCRIPTION ID", "Azure subscription id", required: true
6
+ option "--subscription-cert", "CERTIFICATE", "Path to Azure management certificate", attribute_name: :certificate, required: true
7
+ option "--size", "SIZE", "SIZE", default: 'Small'
8
+ option "--network", "NETWORK", "Virtual Network name"
9
+ option "--subnet", "SUBNET", "Subnet name"
10
+ option "--ssh-key", "SSH KEY", "SSH private key file", required: true
11
+ option "--password", "PASSWORD", "Password"
12
+ option "--location", "LOCATION", "Location", default: 'West Europe'
13
+ option "--version", "VERSION", "Define installed Kontena version", default: 'latest'
14
+
15
+ parameter "[NAME]", "Node name"
16
+
17
+ def execute
18
+ require_api_url
19
+ require_current_grid
20
+
21
+ require 'kontena/machine/azure'
22
+ grid = client(require_token).get("grids/#{current_grid}")
23
+ provisioner = Kontena::Machine::Azure::NodeProvisioner.new(client(require_token), subscription_id, certificate)
24
+ provisioner.run!(
25
+ master_uri: api_url,
26
+ grid_token: grid['token'],
27
+ grid: current_grid,
28
+ password: password,
29
+ ssh_key: ssh_key,
30
+ name: name,
31
+ size: size,
32
+ virtual_network: network,
33
+ subnet: subnet,
34
+ location: location,
35
+ version: version
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ module Kontena::Cli::Nodes::Azure
2
+ class RestartCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ parameter "NAME", "Node name"
6
+ option "--subscription-id", "SUBSCRIPTION ID", "Azure subscription id", required: true
7
+ option "--subscription-cert", "CERTIFICATE", "Path to Azure management certificate", attribute_name: :certificate, required: true
8
+
9
+ def execute
10
+ require_api_url
11
+ require_current_grid
12
+
13
+ require 'kontena/machine/azure'
14
+
15
+ client = ::Azure
16
+ client.management_certificate = certificate
17
+ client.subscription_id = subscription_id
18
+
19
+ client.vm_management.initialize_external_logger(Kontena::Machine::Azure::Logger.new) # We don't want all the output
20
+ ShellSpinner "Restarting Azure VM #{name.colorize(:cyan)} " do
21
+ vm = client.vm_management.get_virtual_machine(name, "kontena-#{current_grid}-#{name}")
22
+ if vm
23
+ client.vm_management.restart_virtual_machine(name, "kontena-#{current_grid}-#{name}")
24
+ else
25
+ abort "\nCannot find Virtual Machine #{name.colorize(:cyan)} in Azure"
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ module Kontena::Cli::Nodes::Azure
2
+ class TerminateCommand < Clamp::Command
3
+ include Kontena::Cli::Common
4
+
5
+ parameter "NAME", "Node name"
6
+ option "--subscription-id", "SUBSCRIPTION ID", "Azure subscription id", required: true
7
+ option "--subscription-cert", "CERTIFICATE", "Path to Azure management certificate", attribute_name: :certificate, required: true
8
+
9
+ def execute
10
+ require_api_url
11
+ require_current_grid
12
+
13
+ require 'kontena/machine/azure'
14
+
15
+ grid = client(require_token).get("grids/#{current_grid}")
16
+ destroyer = Kontena::Machine::Azure::NodeDestroyer.new(client(require_token), subscription_id, certificate)
17
+ destroyer.run!(grid, name)
18
+ end
19
+ end
20
+ end