terraform-wrapper 0.0.2 → 0.2.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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +2 -0
- data/Gemfile +0 -4
- data/lib/terraform-wrapper.rb +36 -16
- data/lib/terraform-wrapper/common.rb +12 -23
- data/lib/terraform-wrapper/shared.rb +7 -1
- data/lib/terraform-wrapper/shared/auths.rb +9 -0
- data/lib/terraform-wrapper/shared/auths/azure.rb +179 -0
- data/lib/terraform-wrapper/shared/auths/common.rb +95 -0
- data/lib/terraform-wrapper/shared/backends/aws.rb +71 -33
- data/lib/terraform-wrapper/shared/backends/azure.rb +38 -39
- data/lib/terraform-wrapper/shared/backends/common.rb +18 -14
- data/lib/terraform-wrapper/shared/backends/local.rb +20 -18
- data/lib/terraform-wrapper/shared/binary.rb +16 -5
- data/lib/terraform-wrapper/shared/code.rb +15 -4
- data/lib/terraform-wrapper/shared/config.rb +61 -31
- data/lib/terraform-wrapper/shared/latest.rb +11 -7
- data/lib/terraform-wrapper/shared/logger.rb +80 -0
- data/lib/terraform-wrapper/shared/logging.rb +77 -0
- data/lib/terraform-wrapper/shared/runner.rb +79 -21
- data/lib/terraform-wrapper/shared/variables.rb +66 -0
- data/lib/terraform-wrapper/tasks.rb +6 -0
- data/lib/terraform-wrapper/tasks/apply.rb +16 -18
- data/lib/terraform-wrapper/tasks/binary.rb +26 -23
- data/lib/terraform-wrapper/tasks/clean.rb +15 -15
- data/lib/terraform-wrapper/tasks/destroy.rb +16 -18
- data/lib/terraform-wrapper/tasks/import.rb +66 -0
- data/lib/terraform-wrapper/tasks/init.rb +16 -18
- data/lib/terraform-wrapper/tasks/plan.rb +16 -18
- data/lib/terraform-wrapper/tasks/plandestroy.rb +16 -18
- data/lib/terraform-wrapper/tasks/upgrade.rb +58 -0
- data/lib/terraform-wrapper/tasks/validate.rb +7 -7
- data/lib/terraform-wrapper/version.rb +1 -1
- data/terraform-wrapper.gemspec +3 -0
- metadata +39 -4
- data/lib/terraform-wrapper/shared/identifiers.rb +0 -70
@@ -14,6 +14,10 @@ module TerraformWrapper
|
|
14
14
|
|
15
15
|
class Config
|
16
16
|
|
17
|
+
###############################################################################
|
18
|
+
|
19
|
+
include TerraformWrapper::Shared::Logging
|
20
|
+
|
17
21
|
###############################################################################
|
18
22
|
|
19
23
|
@@config_exts = [ "", ".yaml", ".yml" ]
|
@@ -25,53 +29,79 @@ module TerraformWrapper
|
|
25
29
|
|
26
30
|
###############################################################################
|
27
31
|
|
32
|
+
attr_reader :auths
|
28
33
|
attr_reader :backend
|
29
34
|
attr_reader :base
|
30
35
|
attr_reader :code
|
31
36
|
attr_reader :name
|
32
37
|
attr_reader :path
|
33
|
-
attr_reader :overrides
|
34
38
|
attr_reader :service
|
35
39
|
attr_reader :variable_files
|
40
|
+
attr_reader :variables
|
36
41
|
|
37
42
|
###############################################################################
|
38
43
|
|
39
|
-
def initialize(
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
44
|
+
def initialize(code:, options:)
|
45
|
+
logger.fatal("Configuration base path must be a String!") unless options["base"].kind_of?(String)
|
46
|
+
logger.fatal("Configuration base path must not be blank!") if options["base"].strip.empty?
|
47
|
+
|
48
|
+
@base = options["base"]
|
49
|
+
|
50
|
+
logger.fatal("Configuration service name must be a String!") unless options["service"].kind_of?(String)
|
51
|
+
logger.fatal("Configuration service name must not be blank!") if options["service"].strip.empty?
|
52
|
+
|
53
|
+
@service = options["service"]
|
54
|
+
|
55
|
+
logger.fatal("Configuration name must be a String!") unless options["name"].kind_of?(String)
|
56
|
+
logger.fatal("Configuration name must not be blank!") if options["name"].strip.empty?
|
57
|
+
|
58
|
+
@name = options["name"]
|
59
|
+
|
60
|
+
logger.fatal("Configuration authenticator for Azure enabled must be a Boolean!") unless [ true, false ].include?(options["auth-azure"])
|
45
61
|
|
46
|
-
|
47
|
-
@code = code
|
48
|
-
@name = name
|
49
|
-
@overrides = overrides
|
50
|
-
@service = service
|
62
|
+
auth_azure = options["auth-azure"]
|
51
63
|
|
64
|
+
logger.fatal("Configuration authenticator for Azure options must be a Hash!") unless options["auth-azure-options"].kind_of?(Hash)
|
65
|
+
|
66
|
+
auth_azure_options = options["auth-azure-options"]
|
67
|
+
|
68
|
+
logger.fatal("Configuration backend name must be a String!") unless options["backend"].kind_of?(String)
|
69
|
+
logger.fatal("Configuration backend name must not be blank!") if options["backend"].strip.empty?
|
70
|
+
|
71
|
+
backend = options["backend"]
|
72
|
+
|
73
|
+
logger.fatal("Configuration backend options must be a Hash!") unless options["backend-options"].kind_of?(Hash)
|
74
|
+
|
75
|
+
backend_options = options["backend-options"]
|
76
|
+
|
77
|
+
@code = code
|
52
78
|
@path = find
|
53
79
|
|
54
80
|
yaml = YAML.load(File.read(@path))
|
55
81
|
|
56
|
-
|
82
|
+
logger.fatal("Invalid YAML in configuration file: #{@path}") unless yaml.kind_of?(Hash)
|
57
83
|
|
58
|
-
|
59
|
-
|
60
|
-
|
84
|
+
if yaml.key?("variables") then
|
85
|
+
logger.fatal("Key 'variables' is not a hash in configuration file: #{@path}") unless yaml["variables"].kind_of?(Hash)
|
86
|
+
@variables = TerraformWrapper::Shared::Variables.new(values: yaml["variables"])
|
87
|
+
else
|
88
|
+
@variables = TerraformWrapper::Shared::Variables.new()
|
89
|
+
end
|
61
90
|
|
62
|
-
|
91
|
+
@variable_files = yaml.key?("terraform") ? validate(variable_files: yaml["terraform"]) : Array.new
|
92
|
+
|
93
|
+
@auths = Array.new
|
94
|
+
@auths.append(TerraformWrapper::Shared::Auths::Azure.new(code: @code, config: @name, options: auth_azure_options, service: @service, variables: @variables)) if auth_azure
|
63
95
|
|
64
96
|
if backend == "local" then
|
65
|
-
@backend = TerraformWrapper::Shared::Backends::Local.new(
|
97
|
+
@backend = TerraformWrapper::Shared::Backends::Local.new(code: @code, config: @name, options: backend_options, service: @service, variables: @variables)
|
66
98
|
elsif backend == "aws" then
|
67
|
-
@backend = TerraformWrapper::Shared::Backends::AWS.new(
|
99
|
+
@backend = TerraformWrapper::Shared::Backends::AWS.new(code: @code, config: @name, options: backend_options, service: @service, variables: @variables)
|
68
100
|
elsif backend == "azure" then
|
69
|
-
@backend = TerraformWrapper::Shared::Backends::Azure.new(
|
101
|
+
@backend = TerraformWrapper::Shared::Backends::Azure.new(code: @code, config: @name, options: backend_options, service: @service, variables: @variables)
|
70
102
|
else
|
71
|
-
|
103
|
+
logger.fatal("Backend: #{backend} is not valid!")
|
72
104
|
end
|
73
|
-
|
74
|
-
@variable_files = yaml.key?("tfvars") ? tfvars(tfvars: yaml["tfvars"]) : Array.new
|
75
105
|
end
|
76
106
|
|
77
107
|
###############################################################################
|
@@ -86,24 +116,24 @@ module TerraformWrapper
|
|
86
116
|
return path if File.file?(path)
|
87
117
|
end
|
88
118
|
|
89
|
-
|
119
|
+
logger.fatal("Terraform configuration name: #{@name} not found in location: #{@base}!")
|
90
120
|
end
|
91
121
|
|
92
122
|
###############################################################################
|
93
123
|
|
94
|
-
def
|
95
|
-
|
124
|
+
def validate(variable_files:)
|
125
|
+
logger.fatal("Optional key 'variable_files' must be a list of strings!") unless variable_files.kind_of?(Array)
|
96
126
|
|
97
127
|
result = Array.new
|
98
128
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
path = File.join(@base, @@variable_files_name,
|
129
|
+
variable_files.each do |variable_file|
|
130
|
+
logger.fatal("All elements of 'variable_files' must be strings!") unless variable_file.kind_of?(String)
|
131
|
+
logger.fatal("All elements of 'variable_files' must not be blank!") if variable_file.strip.empty?
|
132
|
+
path = File.join(@base, @@variable_files_name, variable_file.strip + @@variable_files_ext)
|
103
133
|
if File.file?(path) then
|
104
134
|
result.append(path)
|
105
135
|
else
|
106
|
-
|
136
|
+
logger.fatal("Terraform variables file: #{variable_file}, path: #{path} does not exist!")
|
107
137
|
end
|
108
138
|
end
|
109
139
|
|
@@ -21,6 +21,10 @@ module TerraformWrapper
|
|
21
21
|
|
22
22
|
include Singleton
|
23
23
|
|
24
|
+
###############################################################################
|
25
|
+
|
26
|
+
include TerraformWrapper::Shared::Logging
|
27
|
+
|
24
28
|
###############################################################################
|
25
29
|
|
26
30
|
@version
|
@@ -40,22 +44,22 @@ module TerraformWrapper
|
|
40
44
|
###############################################################################
|
41
45
|
|
42
46
|
def refresh
|
43
|
-
|
47
|
+
logger.info("Finding latest available Terraform release...")
|
44
48
|
|
45
49
|
response = Net::HTTP.get_response(URI("https://checkpoint-api.hashicorp.com/v1/check/terraform"))
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
logger.fatal("Hashicorp Checkpoint did not return status 200 for latest version check!") if response.code != "200"
|
52
|
+
logger.fatal("Response body from Hashicorp Checkpoint is not permitted!") if not response.class.body_permitted?
|
53
|
+
logger.fatal("Response body from Hashicorp Checkpoint is empty!") if response.body.nil?
|
50
54
|
|
51
55
|
body = JSON.parse(response.body)
|
52
56
|
|
53
|
-
|
54
|
-
|
57
|
+
logger.fatal("Hashicorp Checkpoint JSON response did not include latest available Terraform version!") if not body.key?("current_version")
|
58
|
+
logger.fatal("Hashicorp Checkpoint indicated latest available version of Terraform is blank!") if body["current_version"].empty?
|
55
59
|
|
56
60
|
version = body["current_version"]
|
57
61
|
|
58
|
-
|
62
|
+
logger.success("Latest available Terraform release found: #{version}")
|
59
63
|
|
60
64
|
return version
|
61
65
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
###############################################################################
|
6
|
+
|
7
|
+
module TerraformWrapper
|
8
|
+
|
9
|
+
###############################################################################
|
10
|
+
|
11
|
+
module Shared
|
12
|
+
|
13
|
+
###############################################################################
|
14
|
+
|
15
|
+
class Logger < ::Logger
|
16
|
+
|
17
|
+
###############################################################################
|
18
|
+
|
19
|
+
@colour = false
|
20
|
+
|
21
|
+
###############################################################################
|
22
|
+
|
23
|
+
def colour()
|
24
|
+
@colour
|
25
|
+
end
|
26
|
+
|
27
|
+
###############################################################################
|
28
|
+
|
29
|
+
def colour=(enabled)
|
30
|
+
@colour = [ true, false ].include?(enabled) ? enabled : false
|
31
|
+
end
|
32
|
+
|
33
|
+
###############################################################################
|
34
|
+
|
35
|
+
def success(message)
|
36
|
+
info(format(colour: 32, message: message))
|
37
|
+
end
|
38
|
+
|
39
|
+
###############################################################################
|
40
|
+
|
41
|
+
def warn(message)
|
42
|
+
super(format(colour: 33, message: message))
|
43
|
+
end
|
44
|
+
|
45
|
+
###############################################################################
|
46
|
+
|
47
|
+
def error(message)
|
48
|
+
super(format(colour: 31, message: message))
|
49
|
+
end
|
50
|
+
|
51
|
+
###############################################################################
|
52
|
+
|
53
|
+
def fatal(message)
|
54
|
+
super(format(colour: 31, message: message))
|
55
|
+
exit(1)
|
56
|
+
end
|
57
|
+
|
58
|
+
###############################################################################
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
###############################################################################
|
63
|
+
|
64
|
+
def format(colour: 32, message:)
|
65
|
+
return @colour ? "\e[" + colour.to_s + "m" + message + "\e[0m" : message
|
66
|
+
end
|
67
|
+
|
68
|
+
###############################################################################
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
###############################################################################
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
###############################################################################
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
###############################################################################
|
@@ -0,0 +1,77 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
###############################################################################
|
6
|
+
|
7
|
+
module TerraformWrapper
|
8
|
+
|
9
|
+
###############################################################################
|
10
|
+
|
11
|
+
module Shared
|
12
|
+
|
13
|
+
###############################################################################
|
14
|
+
|
15
|
+
module Logging
|
16
|
+
|
17
|
+
###############################################################################
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
###############################################################################
|
22
|
+
|
23
|
+
def logger
|
24
|
+
@logger ||= Logging.logger_for(self.class.name)
|
25
|
+
end
|
26
|
+
|
27
|
+
###############################################################################
|
28
|
+
|
29
|
+
@loggers = {}
|
30
|
+
|
31
|
+
###############################################################################
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
###############################################################################
|
36
|
+
|
37
|
+
def logger_for(classname)
|
38
|
+
@loggers[classname] ||= configure_logger_for(classname)
|
39
|
+
end
|
40
|
+
|
41
|
+
###############################################################################
|
42
|
+
|
43
|
+
def configure_logger_for(classname)
|
44
|
+
colour = ENV["TERRAFORM_WRAPPER_LOG_COLOUR"] || "true"
|
45
|
+
level = ENV["TERRAFORM_WRAPPER_LOG_LEVEL"] || "INFO"
|
46
|
+
|
47
|
+
logger = ::TerraformWrapper::Shared::Logger.new(STDOUT)
|
48
|
+
|
49
|
+
logger.colour = colour.downcase == "true"
|
50
|
+
logger.level = level.upcase
|
51
|
+
logger.progname = classname
|
52
|
+
|
53
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
54
|
+
sevId = severity.chars.first.upcase
|
55
|
+
"[#{sevId}] [#{progname}] #{msg}\n"
|
56
|
+
end
|
57
|
+
|
58
|
+
logger
|
59
|
+
end
|
60
|
+
|
61
|
+
###############################################################################
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
###############################################################################
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
###############################################################################
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
###############################################################################
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
###############################################################################
|
@@ -10,6 +10,10 @@ module TerraformWrapper
|
|
10
10
|
|
11
11
|
class Runner
|
12
12
|
|
13
|
+
###############################################################################
|
14
|
+
|
15
|
+
include TerraformWrapper::Shared::Logging
|
16
|
+
|
13
17
|
###############################################################################
|
14
18
|
|
15
19
|
attr_reader :binary
|
@@ -30,75 +34,111 @@ module TerraformWrapper
|
|
30
34
|
|
31
35
|
###############################################################################
|
32
36
|
|
33
|
-
def download
|
34
|
-
parameters =
|
37
|
+
def download(upgrade: false)
|
38
|
+
parameters = Array.new
|
39
|
+
parameters.append("-backend=false")
|
40
|
+
parameters.append("-upgrade") if upgrade
|
41
|
+
|
35
42
|
@downloaded = run(action: "init", parameters: parameters)
|
36
|
-
|
43
|
+
logger.fatal("Failed to download Terraform modules.") unless @downloaded
|
44
|
+
end
|
45
|
+
|
46
|
+
###############################################################################
|
47
|
+
|
48
|
+
def import(address: nil, id: nil)
|
49
|
+
logger.fatal("Cannot Terraform import before initialising backend!") unless initialised
|
50
|
+
|
51
|
+
logger.fatal("Terraform state address for import must be a String!") unless address.kind_of?(String)
|
52
|
+
logger.fatal("Terraform state address for import must be a String!") unless address.kind_of?(String)
|
53
|
+
logger.fatal("Terraform state address for import must not be blank!") if address.strip.empty?
|
54
|
+
|
55
|
+
logger.fatal("Identification for infrastructure to import must be a String!") unless id.kind_of?(String)
|
56
|
+
logger.fatal("Identification for infrastructure to import must not be blank!") if id.strip.empty?
|
57
|
+
|
58
|
+
parameters = Array.new
|
59
|
+
parameters.concat(variable_files)
|
60
|
+
parameters.concat(variable_strings)
|
61
|
+
|
62
|
+
parameters.append("'#{address}'")
|
63
|
+
parameters.append("'#{id}'")
|
64
|
+
|
65
|
+
logger.fatal("Terraform import failed!") unless run(action: "import", parameters: parameters)
|
37
66
|
end
|
38
67
|
|
39
68
|
###############################################################################
|
40
69
|
|
41
70
|
def init(config:)
|
42
|
-
parameters =
|
71
|
+
parameters = Array.new
|
72
|
+
parameters.append("-reconfigure")
|
73
|
+
|
43
74
|
config.backend.hash.each do |key, value|
|
44
75
|
parameters.append("-backend-config=\"#{key}=#{value}\"")
|
45
76
|
end
|
46
77
|
|
78
|
+
config.auths.map(&:auth)
|
79
|
+
|
47
80
|
@config = config
|
48
81
|
@initialised = run(action: "init", parameters: parameters)
|
49
|
-
|
82
|
+
logger.fatal("Failed to initialise Terraform with backend.") unless @initialised
|
50
83
|
end
|
51
84
|
|
52
85
|
###############################################################################
|
53
86
|
|
54
87
|
def plan(destroy: false, file: nil)
|
55
|
-
|
88
|
+
logger.fatal("Cannot Terraform plan before initialising backend!") unless initialised
|
56
89
|
|
57
|
-
parameters =
|
90
|
+
parameters = Array.new
|
91
|
+
parameters.concat(variable_files)
|
92
|
+
parameters.concat(variable_strings)
|
58
93
|
|
59
94
|
if not file.nil? and file.kind_of?(String) and not file.strip.empty? then
|
60
|
-
|
95
|
+
logger.fatal("Failed to create plan directory: #{directory}") unless ::TerraformWrapper.create_directory(directory: File.dirname(file), purpose: "plan")
|
61
96
|
parameters.append("-out=\"#{file}\"")
|
62
97
|
end
|
63
98
|
|
64
99
|
parameters.append("-destroy") if destroy
|
65
100
|
|
66
|
-
|
101
|
+
logger.fatal("Terraform plan failed!") unless run(action: "plan", parameters: parameters)
|
67
102
|
end
|
68
103
|
|
69
104
|
###############################################################################
|
70
105
|
|
71
106
|
def apply(file: nil)
|
72
|
-
|
107
|
+
logger.fatal("Cannot Terraform apply before initialising backend!") unless initialised
|
73
108
|
|
74
|
-
parameters =
|
109
|
+
parameters = Array.new
|
110
|
+
parameters.concat(variable_files)
|
111
|
+
parameters.concat(variable_strings)
|
112
|
+
parameters.append("-auto-approve")
|
75
113
|
|
76
114
|
if not file.nil? and file.kind_of?(String) and not file.strip.empty? then
|
77
|
-
|
115
|
+
logger.fatal("Plan file: #{file} does not exist!") unless File.file?(file)
|
78
116
|
parameters.append("\"#{file}\"")
|
79
117
|
else
|
80
118
|
parameters.concat(variable_files)
|
81
119
|
end
|
82
120
|
|
83
|
-
|
121
|
+
logger.fatal("Terraform apply failed!") unless run(action: "apply", parameters: parameters)
|
84
122
|
end
|
85
123
|
|
86
124
|
###############################################################################
|
87
125
|
|
88
126
|
def destroy
|
89
|
-
|
127
|
+
logger.fatal("Cannot Terraform destroy before initialising backend!") unless initialised
|
90
128
|
|
91
|
-
parameters =
|
129
|
+
parameters = Array.new
|
92
130
|
parameters.concat(variable_files)
|
131
|
+
parameters.concat(variable_strings)
|
132
|
+
parameters.append("-auto-approve")
|
93
133
|
|
94
|
-
|
134
|
+
logger.fatal("Terraform destroy failed!") unless run(action: "destroy", parameters: parameters)
|
95
135
|
end
|
96
136
|
|
97
137
|
###############################################################################
|
98
138
|
|
99
139
|
def validate
|
100
|
-
|
101
|
-
|
140
|
+
logger.fatal("Cannot Terraform validate before downloading modules!") unless downloaded
|
141
|
+
logger.fatal("Terraform validation failed!") unless run(action: "validate")
|
102
142
|
end
|
103
143
|
|
104
144
|
###############################################################################
|
@@ -108,7 +148,7 @@ module TerraformWrapper
|
|
108
148
|
###############################################################################
|
109
149
|
|
110
150
|
def variable_files
|
111
|
-
|
151
|
+
logger.fatal("Cannot generate variable files until Terraform has been initialised!") unless @initialised
|
112
152
|
|
113
153
|
result = Array.new
|
114
154
|
|
@@ -119,6 +159,24 @@ module TerraformWrapper
|
|
119
159
|
return result
|
120
160
|
end
|
121
161
|
|
162
|
+
###############################################################################
|
163
|
+
|
164
|
+
def variable_strings
|
165
|
+
logger.fatal("Cannot generate variable strings until Terraform has been initialised!") unless @initialised
|
166
|
+
|
167
|
+
result = Array.new
|
168
|
+
|
169
|
+
result.append("-var=\"component=#{@code.name}\"")
|
170
|
+
result.append("-var=\"config=#{@config.name}\"")
|
171
|
+
result.append("-var=\"service=#{@config.service}\"")
|
172
|
+
|
173
|
+
@config.variables.values.each do |key, value|
|
174
|
+
result.append("-var=\"#{key.to_s}=#{value}\"")
|
175
|
+
end
|
176
|
+
|
177
|
+
return result
|
178
|
+
end
|
179
|
+
|
122
180
|
###############################################################################
|
123
181
|
|
124
182
|
def run(action:, parameters: Array.new)
|
@@ -128,14 +186,14 @@ module TerraformWrapper
|
|
128
186
|
|
129
187
|
cmdline = [ "\"#{@binary.path}\"", action ].concat(parameters).join(" ")
|
130
188
|
|
131
|
-
|
189
|
+
logger.info("Starting Terraform, action: #{action}")
|
132
190
|
|
133
191
|
puts("\n" + ('#' * 80) + "\n\n")
|
134
192
|
|
135
193
|
Dir.chdir(@code.path)
|
136
194
|
result = system(cmdline) || false
|
137
195
|
|
138
|
-
puts("\n")
|
196
|
+
puts("\n" + ('#' * 80) + "\n\n")
|
139
197
|
|
140
198
|
return result
|
141
199
|
end
|