glb 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +1 -0
- data/README.md +51 -0
- data/Rakefile +6 -0
- data/docs/external.md +19 -0
- data/docs/internal.md +65 -0
- data/docs/ssl.md +33 -0
- data/docs/static-ip-address.md +30 -0
- data/exe/glb +14 -0
- data/glb.gemspec +33 -0
- data/lib/glb/autoloader.rb +21 -0
- data/lib/glb/cli/base.rb +15 -0
- data/lib/glb/cli/help/completion.md +20 -0
- data/lib/glb/cli/help/completion_script.md +3 -0
- data/lib/glb/cli/help.rb +11 -0
- data/lib/glb/cli.rb +48 -0
- data/lib/glb/command.rb +91 -0
- data/lib/glb/completer/script.rb +8 -0
- data/lib/glb/completer/script.sh +10 -0
- data/lib/glb/completer.rb +159 -0
- data/lib/glb/config.rb +147 -0
- data/lib/glb/core.rb +27 -0
- data/lib/glb/lb/args.rb +66 -0
- data/lib/glb/lb/backend_service/backend.rb +94 -0
- data/lib/glb/lb/backend_service.rb +19 -0
- data/lib/glb/lb/firewall_rule.rb +9 -0
- data/lib/glb/lb/forwarding_rule.rb +16 -0
- data/lib/glb/lb/forwarding_rule_https.rb +24 -0
- data/lib/glb/lb/health_check.rb +13 -0
- data/lib/glb/lb/names.rb +50 -0
- data/lib/glb/lb/resource.rb +119 -0
- data/lib/glb/lb/target_http_proxy.rb +9 -0
- data/lib/glb/lb/target_https_proxy.rb +4 -0
- data/lib/glb/lb/url_map.rb +9 -0
- data/lib/glb/lb.rb +110 -0
- data/lib/glb/util/sh.rb +37 -0
- data/lib/glb/util/sure.rb +18 -0
- data/lib/glb/version.rb +3 -0
- data/lib/glb.rb +22 -0
- data/spec/cli_spec.rb +26 -0
- data/spec/spec_helper.rb +29 -0
- metadata +245 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9e9b2940b5d66eed7e9a2a998d8f2a6d38a0407277d22fdc242371b44235e5cc
|
4
|
+
data.tar.gz: 21bc007d1f5347d8948492e291ba7f0d238d4c7005124b367ab14bf9e423ab7a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6e9a3a1d25a8ccb9ab4d03eb35d049db46f67a1dfe5562a839840a6c510f5bda518c261dcc02d3dc0cb970e2a8ff3ef2e6bf5ae50137f1fa223b77f927b5056b
|
7
|
+
data.tar.gz: 3b1a02edecf4226c17708d3d92d541e3f5ac2ddb4ebfa8a79687f7833a8da139333970db4685cf7d7e9cd89eac98082a7e2ffd6ba90850ced95012586a451f44
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
guard "bundler", cmd: "bundle" do
|
2
|
+
watch("Gemfile")
|
3
|
+
watch(/^.+\.gemspec/)
|
4
|
+
end
|
5
|
+
|
6
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
7
|
+
require "guard/rspec/dsl"
|
8
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
9
|
+
|
10
|
+
# RSpec files
|
11
|
+
rspec = dsl.rspec
|
12
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
13
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
14
|
+
watch(rspec.spec_files)
|
15
|
+
|
16
|
+
# Ruby files
|
17
|
+
ruby = dsl.ruby
|
18
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
19
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Proprietary, All rights reserved. For licensing and terms, please refer to https://www.boltops.com/terms
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Glb
|
2
|
+
|
3
|
+
Create and delete Google Load Balancer components.
|
4
|
+
|
5
|
+
Pros:
|
6
|
+
|
7
|
+
* The tool wraps gcloud commands. This helps those who are familiar with gcloud commands and are referencing google cloud docs.
|
8
|
+
|
9
|
+
Cons/Limitations:
|
10
|
+
|
11
|
+
* The tool assumes that the source of truth is the configuration. It does not detect and will update and overwrite any manual changes that does not match the configuration.
|
12
|
+
* This is notably different from terraform which will perform a diff calculation, which can provide a diff in the plan.
|
13
|
+
* The `gcloud compute [RESOURCE] update` will not run if there are no attributes in the command, else `gcloud` reports an error.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Commands:
|
18
|
+
|
19
|
+
glb plan APP
|
20
|
+
glb up APP
|
21
|
+
glb down APP
|
22
|
+
glb show APP
|
23
|
+
|
24
|
+
APP is your app name. IE: demo
|
25
|
+
|
26
|
+
## Docs
|
27
|
+
|
28
|
+
* [External Load Balancer (Global)](docs/external.md)
|
29
|
+
* [Internal Load Balancer (Region)](docs/internal.md)
|
30
|
+
|
31
|
+
## Resources
|
32
|
+
|
33
|
+
The tool creates these resources:
|
34
|
+
|
35
|
+
* firewall rule
|
36
|
+
* health check
|
37
|
+
* backend service
|
38
|
+
* url map
|
39
|
+
* target http proxy
|
40
|
+
* forwarding rule
|
41
|
+
|
42
|
+
If SSL is enabled it'll also create a
|
43
|
+
|
44
|
+
* target https proxy (associated with the same url map)
|
45
|
+
* forwarding rule (associated with the target https proxy)
|
46
|
+
|
47
|
+
The same url map is used because that's what shows up as a Load Balancer in the Google console.
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
|
51
|
+
gem install glb
|
data/Rakefile
ADDED
data/docs/external.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# External Load Balancer (global)
|
2
|
+
|
3
|
+
.glb/config.rb
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
Glb.configure do |config|
|
7
|
+
config.firewall_rule.network = "dev"
|
8
|
+
config.firewall_rule.target_tags = "demo-web-dev"
|
9
|
+
config.firewall_rule.rules = "tcp:8080"
|
10
|
+
|
11
|
+
config.network_endpoint_group = "demo-web-dev-80-neg"
|
12
|
+
|
13
|
+
config.health_check.port = 8080
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
To create load balancer run:
|
18
|
+
|
19
|
+
glb up demo
|
data/docs/internal.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Internal Load Balancer (region)
|
2
|
+
|
3
|
+
Internal load balancers require a configuration that includes region, network, and subnet. It also requires a firewall rule allowing the access from the load balancer proxy only subnet.
|
4
|
+
|
5
|
+
## Recommended config.lb shorthand
|
6
|
+
|
7
|
+
Here's an example with the suggested `config.rb` options:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
Glb.configure do |config|
|
11
|
+
config.firewall_rule.target_tags = "gke-dev-cluster-9696112c-node"
|
12
|
+
config.firewall_rule.rules = "tcp:8080"
|
13
|
+
config.health_check.port = 8080
|
14
|
+
config.backend_service.add_backend.network_endpoint_group = "demo-web-dev-80-neg"
|
15
|
+
|
16
|
+
# ranges:
|
17
|
+
# load balancer health check: 130.211.0.0/22,35.191.0.0/16
|
18
|
+
# dev-proxy: 10.80.1.0/24
|
19
|
+
config.firewall_rule.source_ranges = "130.211.0.0/22,35.191.0.0/16,10.80.1.0/24"
|
20
|
+
|
21
|
+
# internal load balancer shorthand config.lb. maps to health_check, backend_service, url_map, target_http_proxy, forwarding_rule options
|
22
|
+
config.lb.region = "us-central1"
|
23
|
+
config.lb.load_balancing_scheme = "INTERNAL_MANAGED"
|
24
|
+
config.lb.network = "dev"
|
25
|
+
config.lb.subnet = "dev-app"
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
## Explicit config long form
|
30
|
+
|
31
|
+
You can also more explicitly set each component options. The previous config is the same as this one below. The `config.lb` options simply map to the underlying resource options.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Glb.configure do |config|
|
35
|
+
config.firewall_rule.target_tags = "gke-dev-cluster-9696112c-node"
|
36
|
+
config.firewall_rule.rules = "tcp:8080"
|
37
|
+
config.health_check.port = 8080
|
38
|
+
config.backend_service.add_backend.network_endpoint_group = "demo-web-dev-80-neg"
|
39
|
+
|
40
|
+
# ranges:
|
41
|
+
# load balancer health check: 130.211.0.0/22,35.191.0.0/16
|
42
|
+
# dev-proxy: 10.80.1.0/24
|
43
|
+
# IMPORTANT: allow the proxy only subnet to access in the firewall rule
|
44
|
+
# or else you won't be able to reach the internal LB IP
|
45
|
+
config.firewall_rule.source_ranges = "130.211.0.0/22,35.191.0.0/16,10.80.1.0/24"
|
46
|
+
|
47
|
+
# internal load balancer: specific resource options
|
48
|
+
config.health_check.region = "us-central1"
|
49
|
+
config.backend_service.load_balancing_scheme = "INTERNAL_MANAGED"
|
50
|
+
config.backend_service.region = "us-central1"
|
51
|
+
config.backend_service.health_checks_region = "us-central1"
|
52
|
+
config.backend_service.add_backend.region = "us-central1"
|
53
|
+
config.url_map.region = "us-central1" # not updatable
|
54
|
+
config.target_http_proxy.region = "us-central1"
|
55
|
+
config.forwarding_rule.region = "us-central1"
|
56
|
+
config.forwarding_rule.network = "dev"
|
57
|
+
config.forwarding_rule.subnet = "dev-app"
|
58
|
+
config.forwarding_rule.load_balancing_scheme = "INTERNAL_MANAGED"
|
59
|
+
config.forwarding_rule.target_http_proxy_region = "us-central1"
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
To create load balancer run:
|
64
|
+
|
65
|
+
glb up demo
|
data/docs/ssl.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# SSL or HTTPS support
|
2
|
+
|
3
|
+
Below are examples on how to configure ssl.
|
4
|
+
|
5
|
+
## External (global)
|
6
|
+
|
7
|
+
Create the google managed cert.
|
8
|
+
|
9
|
+
gcloud compute ssl-certificates create demo-dev --global --domains demo-dev.example.com
|
10
|
+
|
11
|
+
.glb/config.rb
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
Glb.configure do |config|
|
15
|
+
config.lb.ssl_enabled = true
|
16
|
+
config.lb.ssl_certificates = "demo-dev"
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
## Internal (region)
|
21
|
+
|
22
|
+
Create the google unmanaged cert.
|
23
|
+
|
24
|
+
gcloud compute ssl-certificates create demo-dev --certificate certificate.crt --certificate ca_bundle.crt --private-key private.key
|
25
|
+
|
26
|
+
.glb/config.rb
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Glb.configure do |config|
|
30
|
+
config.lb.ssl_enabled = true
|
31
|
+
config.lb.ssl_certificates = "demo-dev"
|
32
|
+
end
|
33
|
+
```
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Static IP Address
|
2
|
+
|
3
|
+
## External (global)
|
4
|
+
|
5
|
+
To configure a pre-allocated static ip address:
|
6
|
+
|
7
|
+
gcloud compute addresses create demo-dev --global
|
8
|
+
gcloud compute addresses create demo-https-dev --global # if using https
|
9
|
+
|
10
|
+
.glb/config.rb
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
Glb.configure do |config|
|
14
|
+
config.firewall_rule.network = "dev"
|
15
|
+
config.firewall_rule.target_tags = "demo-web-dev"
|
16
|
+
config.firewall_rule.rules = "tcp:8080"
|
17
|
+
|
18
|
+
config.network_endpoint_group = "demo-web-dev-80-neg"
|
19
|
+
|
20
|
+
config.health_check.port = 8080
|
21
|
+
|
22
|
+
config.forwarding_rule.address = "demo-dev"
|
23
|
+
# config.forwarding_rule_https.address = "demo-https-dev" # if using https
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
IMPORTANT: The [gcloud compute forwarding-rules update](https://cloud.google.com/sdk/gcloud/reference/compute/forwarding-rules/update) command does not support updating the static IP address. You have to delete the forwarding rule and recreate it.
|
28
|
+
|
29
|
+
gcloud compute forwarding-rules delete demo-dev --global
|
30
|
+
|
data/exe/glb
ADDED
data/glb.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "glb/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "glb"
|
8
|
+
spec.version = Glb::VERSION
|
9
|
+
spec.authors = ["Tung Nguyen"]
|
10
|
+
spec.email = ["tongueroo@gmail.com"]
|
11
|
+
spec.summary = "Google Load Balanacer Tool"
|
12
|
+
spec.homepage = "https://github.com/boltops-tools/glb"
|
13
|
+
spec.license = "Apache2.0"
|
14
|
+
|
15
|
+
spec.files = File.directory?('.git') ? `git ls-files`.split($/) : Dir.glob("**/*")
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activesupport"
|
22
|
+
spec.add_dependency "dsl_evaluator"
|
23
|
+
spec.add_dependency "memoist"
|
24
|
+
spec.add_dependency "rainbow"
|
25
|
+
spec.add_dependency "thor"
|
26
|
+
spec.add_dependency "zeitwerk"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler"
|
29
|
+
spec.add_development_dependency "byebug"
|
30
|
+
spec.add_development_dependency "cli_markdown"
|
31
|
+
spec.add_development_dependency "rake"
|
32
|
+
spec.add_development_dependency "rspec"
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
|
3
|
+
module Glb
|
4
|
+
class Autoloader
|
5
|
+
class Inflector < Zeitwerk::Inflector
|
6
|
+
def camelize(basename, _abspath)
|
7
|
+
map = { cli: "CLI", version: "VERSION" }
|
8
|
+
map[basename.to_sym] || super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def setup
|
14
|
+
loader = Zeitwerk::Loader.new
|
15
|
+
loader.inflector = Inflector.new
|
16
|
+
loader.push_dir(File.dirname(__dir__)) # lib
|
17
|
+
loader.setup
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/glb/cli/base.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class Glb::CLI
|
2
|
+
class Base
|
3
|
+
include Glb::Util::Sh
|
4
|
+
|
5
|
+
attr_reader :options
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
@name = options[:name] # IE: demo-web-dev
|
9
|
+
end
|
10
|
+
|
11
|
+
def region
|
12
|
+
@options[:region] || ENV['GOOGLE_REGION'] || "us-central1"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
## Examples
|
2
|
+
|
3
|
+
glb completion
|
4
|
+
|
5
|
+
Prints words for TAB auto-completion.
|
6
|
+
|
7
|
+
glb completion
|
8
|
+
glb completion hello
|
9
|
+
glb completion hello name
|
10
|
+
|
11
|
+
To enable, TAB auto-completion add the following to your profile:
|
12
|
+
|
13
|
+
eval $(glb completion_script)
|
14
|
+
|
15
|
+
Auto-completion example usage:
|
16
|
+
|
17
|
+
glb [TAB]
|
18
|
+
glb hello [TAB]
|
19
|
+
glb hello name [TAB]
|
20
|
+
glb hello name --[TAB]
|
data/lib/glb/cli/help.rb
ADDED
data/lib/glb/cli.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Glb
|
2
|
+
class CLI < Command
|
3
|
+
class_option :verbose, type: :boolean
|
4
|
+
class_option :noop, type: :boolean
|
5
|
+
|
6
|
+
desc "plan", "plan load balancer"
|
7
|
+
long_desc Help.text("plan")
|
8
|
+
def plan(name)
|
9
|
+
Glb::Lb.new(@options.merge(name: name)).plan
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "up", "create or update load balancer"
|
13
|
+
long_desc Help.text("up")
|
14
|
+
def up(name)
|
15
|
+
Glb::Lb.new(@options.merge(name: name)).up
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "show", "show load balancer"
|
19
|
+
long_desc Help.text("show")
|
20
|
+
option :format, desc: "formats: config, csv, default, diff, disable, flattened, get, json, list, multi, none, object, table, text, value, yaml"
|
21
|
+
def show(name)
|
22
|
+
Glb::Lb.new(@options.merge(name: name)).show
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "down", "down load balancer"
|
26
|
+
long_desc Help.text("down")
|
27
|
+
def down(name)
|
28
|
+
Glb::Lb.new(@options.merge(name: name)).down
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "completion *PARAMS", "Prints words for auto-completion."
|
32
|
+
long_desc Help.text(:completion)
|
33
|
+
def completion(*params)
|
34
|
+
Completer.new(CLI, *params).run
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "completion_script", "Generates a script that can be eval to setup auto-completion."
|
38
|
+
long_desc Help.text(:completion_script)
|
39
|
+
def completion_script
|
40
|
+
Completer::Script.generate
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "version", "prints version"
|
44
|
+
def version
|
45
|
+
puts VERSION
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/glb/command.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
# Override thor's long_desc identation behavior
|
4
|
+
# https://github.com/erikhuda/thor/issues/398
|
5
|
+
class Thor
|
6
|
+
module Shell
|
7
|
+
class Basic
|
8
|
+
def print_wrapped(message, options = {})
|
9
|
+
message = "\n#{message}" unless message[0] == "\n"
|
10
|
+
stdout.puts message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Glb
|
17
|
+
class Command < Thor
|
18
|
+
class_option :yes, aliases: :y, type: :boolean
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def dispatch(m, args, options, config)
|
22
|
+
# Allow calling for help via:
|
23
|
+
# glb command help
|
24
|
+
# glb command -h
|
25
|
+
# glb command --help
|
26
|
+
# glb command -D
|
27
|
+
#
|
28
|
+
# as well thor's normal way:
|
29
|
+
#
|
30
|
+
# glb help command
|
31
|
+
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
32
|
+
if args.length > 1 && !(args & help_flags).empty?
|
33
|
+
args -= help_flags
|
34
|
+
args.insert(-2, "help")
|
35
|
+
end
|
36
|
+
|
37
|
+
# glb version
|
38
|
+
# glb --version
|
39
|
+
# glb -v
|
40
|
+
version_flags = ["--version", "-v"]
|
41
|
+
if args.length == 1 && !(args & version_flags).empty?
|
42
|
+
args = ["version"]
|
43
|
+
end
|
44
|
+
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
# Override command_help to include the description at the top of the
|
49
|
+
# long_description.
|
50
|
+
def command_help(shell, command_name)
|
51
|
+
meth = normalize_command_name(command_name)
|
52
|
+
command = all_commands[meth]
|
53
|
+
alter_command_description(command)
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
def alter_command_description(command)
|
58
|
+
return unless command
|
59
|
+
|
60
|
+
# Add description to beginning of long_description
|
61
|
+
long_desc = if command.long_description
|
62
|
+
"#{command.description}\n\n#{command.long_description}"
|
63
|
+
else
|
64
|
+
command.description
|
65
|
+
end
|
66
|
+
|
67
|
+
# add reference url to end of the long_description
|
68
|
+
unless website.empty?
|
69
|
+
full_command = [command.ancestor_name, command.name].compact.join('-')
|
70
|
+
url = "#{website}/reference/glb-#{full_command}"
|
71
|
+
long_desc += "\n\nHelp also available at: #{url}"
|
72
|
+
end
|
73
|
+
|
74
|
+
command.long_description = long_desc
|
75
|
+
end
|
76
|
+
private :alter_command_description
|
77
|
+
|
78
|
+
# meant to be overriden
|
79
|
+
def website
|
80
|
+
""
|
81
|
+
end
|
82
|
+
|
83
|
+
# https://github.com/erikhuda/thor/issues/244
|
84
|
+
# Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `Lono::CLI`
|
85
|
+
# You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
|
86
|
+
def exit_on_failure?
|
87
|
+
true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|