openbolt 5.0.0.rc1
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/Puppetfile +52 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +60 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/containerresult.rb +51 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/future.rb +25 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +71 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +55 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +65 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +93 -0
- data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +33 -0
- data/bolt-modules/boltlib/lib/puppet/functions/add_to_group.rb +38 -0
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +208 -0
- data/bolt-modules/boltlib/lib/puppet/functions/background.rb +62 -0
- data/bolt-modules/boltlib/lib/puppet/functions/catch_errors.rb +57 -0
- data/bolt-modules/boltlib/lib/puppet/functions/download_file.rb +130 -0
- data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +31 -0
- data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +52 -0
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +87 -0
- data/bolt-modules/boltlib/lib/puppet/functions/get_target.rb +34 -0
- data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +35 -0
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +74 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_command.rb +97 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_fact.rb +47 -0
- data/bolt-modules/boltlib/lib/puppet/functions/puppetdb_query.rb +52 -0
- data/bolt-modules/boltlib/lib/puppet/functions/remove_from_group.rb +40 -0
- data/bolt-modules/boltlib/lib/puppet/functions/resolve_references.rb +42 -0
- data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +53 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +106 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_container.rb +162 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +291 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +145 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +164 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +211 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_config.rb +48 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_feature.rb +43 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +145 -0
- data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +38 -0
- data/bolt-modules/boltlib/lib/puppet/functions/upload_file.rb +101 -0
- data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +29 -0
- data/bolt-modules/boltlib/lib/puppet/functions/wait.rb +131 -0
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +59 -0
- data/bolt-modules/boltlib/lib/puppet/functions/without_default_logging.rb +39 -0
- data/bolt-modules/boltlib/lib/puppet/functions/write_file.rb +50 -0
- data/bolt-modules/boltlib/types/planresult.pp +18 -0
- data/bolt-modules/boltlib/types/targetspec.pp +7 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/do_until.rb +42 -0
- data/bolt-modules/ctrl/lib/puppet/functions/ctrl/sleep.rb +20 -0
- data/bolt-modules/dir/lib/puppet/functions/dir/children.rb +35 -0
- data/bolt-modules/file/lib/puppet/functions/file/delete.rb +21 -0
- data/bolt-modules/file/lib/puppet/functions/file/exists.rb +28 -0
- data/bolt-modules/file/lib/puppet/functions/file/join.rb +20 -0
- data/bolt-modules/file/lib/puppet/functions/file/read.rb +33 -0
- data/bolt-modules/file/lib/puppet/functions/file/readable.rb +28 -0
- data/bolt-modules/file/lib/puppet/functions/file/write.rb +24 -0
- data/bolt-modules/log/lib/puppet/functions/log/debug.rb +39 -0
- data/bolt-modules/log/lib/puppet/functions/log/error.rb +40 -0
- data/bolt-modules/log/lib/puppet/functions/log/fatal.rb +40 -0
- data/bolt-modules/log/lib/puppet/functions/log/info.rb +39 -0
- data/bolt-modules/log/lib/puppet/functions/log/trace.rb +39 -0
- data/bolt-modules/log/lib/puppet/functions/log/warn.rb +41 -0
- data/bolt-modules/out/lib/puppet/functions/out/message.rb +36 -0
- data/bolt-modules/out/lib/puppet/functions/out/verbose.rb +35 -0
- data/bolt-modules/prompt/lib/puppet/functions/prompt/menu.rb +103 -0
- data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +65 -0
- data/bolt-modules/system/lib/puppet/functions/system/env.rb +20 -0
- data/exe/bolt +17 -0
- data/guides/debugging.yaml +27 -0
- data/guides/inventory.yaml +23 -0
- data/guides/links.yaml +12 -0
- data/guides/logging.yaml +17 -0
- data/guides/module.yaml +18 -0
- data/guides/modulepath.yaml +24 -0
- data/guides/project.yaml +21 -0
- data/guides/targets.yaml +28 -0
- data/guides/transports.yaml +22 -0
- data/lib/bolt/analytics.rb +233 -0
- data/lib/bolt/application.rb +806 -0
- data/lib/bolt/applicator.rb +368 -0
- data/lib/bolt/apply_inventory.rb +93 -0
- data/lib/bolt/apply_result.rb +154 -0
- data/lib/bolt/apply_target.rb +90 -0
- data/lib/bolt/bolt_option_parser.rb +1226 -0
- data/lib/bolt/catalog/logging.rb +15 -0
- data/lib/bolt/catalog.rb +144 -0
- data/lib/bolt/cli.rb +949 -0
- data/lib/bolt/config/modulepath.rb +30 -0
- data/lib/bolt/config/options.rb +673 -0
- data/lib/bolt/config/transport/base.rb +133 -0
- data/lib/bolt/config/transport/docker.rb +34 -0
- data/lib/bolt/config/transport/jail.rb +33 -0
- data/lib/bolt/config/transport/local.rb +39 -0
- data/lib/bolt/config/transport/lxd.rb +34 -0
- data/lib/bolt/config/transport/options.rb +431 -0
- data/lib/bolt/config/transport/orch.rb +41 -0
- data/lib/bolt/config/transport/podman.rb +33 -0
- data/lib/bolt/config/transport/remote.rb +24 -0
- data/lib/bolt/config/transport/ssh.rb +138 -0
- data/lib/bolt/config/transport/winrm.rb +63 -0
- data/lib/bolt/config.rb +515 -0
- data/lib/bolt/container_result.rb +105 -0
- data/lib/bolt/error.rb +194 -0
- data/lib/bolt/executor.rb +539 -0
- data/lib/bolt/fiber_executor.rb +190 -0
- data/lib/bolt/inventory/group.rb +446 -0
- data/lib/bolt/inventory/inventory.rb +391 -0
- data/lib/bolt/inventory/options.rb +139 -0
- data/lib/bolt/inventory/target.rb +293 -0
- data/lib/bolt/inventory.rb +120 -0
- data/lib/bolt/logger.rb +252 -0
- data/lib/bolt/module.rb +54 -0
- data/lib/bolt/module_installer/installer.rb +44 -0
- data/lib/bolt/module_installer/puppetfile/forge_module.rb +54 -0
- data/lib/bolt/module_installer/puppetfile/git_module.rb +37 -0
- data/lib/bolt/module_installer/puppetfile/module.rb +26 -0
- data/lib/bolt/module_installer/puppetfile.rb +131 -0
- data/lib/bolt/module_installer/resolver.rb +129 -0
- data/lib/bolt/module_installer/specs/forge_spec.rb +91 -0
- data/lib/bolt/module_installer/specs/git_spec.rb +150 -0
- data/lib/bolt/module_installer/specs/id/base.rb +116 -0
- data/lib/bolt/module_installer/specs/id/gitclone.rb +120 -0
- data/lib/bolt/module_installer/specs/id/github.rb +90 -0
- data/lib/bolt/module_installer/specs/id/gitlab.rb +92 -0
- data/lib/bolt/module_installer/specs.rb +95 -0
- data/lib/bolt/module_installer.rb +208 -0
- data/lib/bolt/node/errors.rb +55 -0
- data/lib/bolt/node/output.rb +29 -0
- data/lib/bolt/outputter/human.rb +958 -0
- data/lib/bolt/outputter/json.rb +205 -0
- data/lib/bolt/outputter/logger.rb +76 -0
- data/lib/bolt/outputter/rainbow.rb +118 -0
- data/lib/bolt/outputter.rb +57 -0
- data/lib/bolt/pal/issues.rb +19 -0
- data/lib/bolt/pal/logging.rb +17 -0
- data/lib/bolt/pal/yaml_plan/evaluator.rb +83 -0
- data/lib/bolt/pal/yaml_plan/loader.rb +94 -0
- data/lib/bolt/pal/yaml_plan/parameter.rb +63 -0
- data/lib/bolt/pal/yaml_plan/step/command.rb +45 -0
- data/lib/bolt/pal/yaml_plan/step/download.rb +37 -0
- data/lib/bolt/pal/yaml_plan/step/eval.rb +42 -0
- data/lib/bolt/pal/yaml_plan/step/message.rb +31 -0
- data/lib/bolt/pal/yaml_plan/step/plan.rb +42 -0
- data/lib/bolt/pal/yaml_plan/step/resources.rb +170 -0
- data/lib/bolt/pal/yaml_plan/step/script.rb +62 -0
- data/lib/bolt/pal/yaml_plan/step/task.rb +42 -0
- data/lib/bolt/pal/yaml_plan/step/upload.rb +37 -0
- data/lib/bolt/pal/yaml_plan/step/verbose.rb +31 -0
- data/lib/bolt/pal/yaml_plan/step.rb +223 -0
- data/lib/bolt/pal/yaml_plan/transpiler.rb +90 -0
- data/lib/bolt/pal/yaml_plan.rb +172 -0
- data/lib/bolt/pal.rb +847 -0
- data/lib/bolt/plan_creator.rb +219 -0
- data/lib/bolt/plan_future.rb +86 -0
- data/lib/bolt/plan_result.rb +44 -0
- data/lib/bolt/plugin/cache.rb +76 -0
- data/lib/bolt/plugin/env_var.rb +54 -0
- data/lib/bolt/plugin/module.rb +276 -0
- data/lib/bolt/plugin/prompt.rb +36 -0
- data/lib/bolt/plugin/puppet_connect_data.rb +84 -0
- data/lib/bolt/plugin/puppetdb.rb +124 -0
- data/lib/bolt/plugin/task.rb +72 -0
- data/lib/bolt/plugin.rb +380 -0
- data/lib/bolt/project.rb +219 -0
- data/lib/bolt/project_manager/config_migrator.rb +113 -0
- data/lib/bolt/project_manager/inventory_migrator.rb +67 -0
- data/lib/bolt/project_manager/migrator.rb +39 -0
- data/lib/bolt/project_manager/module_migrator.rb +203 -0
- data/lib/bolt/project_manager.rb +221 -0
- data/lib/bolt/puppetdb/client.rb +153 -0
- data/lib/bolt/puppetdb/config.rb +176 -0
- data/lib/bolt/puppetdb/instance.rb +146 -0
- data/lib/bolt/puppetdb.rb +15 -0
- data/lib/bolt/r10k_log_proxy.rb +30 -0
- data/lib/bolt/rerun.rb +55 -0
- data/lib/bolt/resource_instance.rb +133 -0
- data/lib/bolt/result.rb +247 -0
- data/lib/bolt/result_set.rb +128 -0
- data/lib/bolt/shell/bash/tmpdir.rb +62 -0
- data/lib/bolt/shell/bash.rb +516 -0
- data/lib/bolt/shell/powershell/snippets.rb +181 -0
- data/lib/bolt/shell/powershell.rb +365 -0
- data/lib/bolt/shell.rb +105 -0
- data/lib/bolt/target.rb +174 -0
- data/lib/bolt/task/puppet_server.rb +27 -0
- data/lib/bolt/task/run.rb +55 -0
- data/lib/bolt/task.rb +163 -0
- data/lib/bolt/transport/base.rb +252 -0
- data/lib/bolt/transport/docker/connection.rb +150 -0
- data/lib/bolt/transport/docker.rb +23 -0
- data/lib/bolt/transport/jail/connection.rb +81 -0
- data/lib/bolt/transport/jail.rb +21 -0
- data/lib/bolt/transport/local/connection.rb +106 -0
- data/lib/bolt/transport/local.rb +20 -0
- data/lib/bolt/transport/lxd/connection.rb +115 -0
- data/lib/bolt/transport/lxd.rb +26 -0
- data/lib/bolt/transport/orch/connection.rb +111 -0
- data/lib/bolt/transport/orch.rb +271 -0
- data/lib/bolt/transport/podman/connection.rb +102 -0
- data/lib/bolt/transport/podman.rb +19 -0
- data/lib/bolt/transport/remote.rb +41 -0
- data/lib/bolt/transport/simple.rb +54 -0
- data/lib/bolt/transport/ssh/connection.rb +321 -0
- data/lib/bolt/transport/ssh/exec_connection.rb +140 -0
- data/lib/bolt/transport/ssh.rb +48 -0
- data/lib/bolt/transport/winrm/connection.rb +378 -0
- data/lib/bolt/transport/winrm.rb +33 -0
- data/lib/bolt/util/format.rb +68 -0
- data/lib/bolt/util/puppet_log_level.rb +21 -0
- data/lib/bolt/util.rb +465 -0
- data/lib/bolt/validator.rb +227 -0
- data/lib/bolt/version.rb +5 -0
- data/lib/bolt.rb +8 -0
- data/lib/bolt_server/acl.rb +39 -0
- data/lib/bolt_server/base_config.rb +112 -0
- data/lib/bolt_server/config.rb +64 -0
- data/lib/bolt_server/file_cache.rb +200 -0
- data/lib/bolt_server/request_error.rb +11 -0
- data/lib/bolt_server/schemas/action-check_node_connections.json +14 -0
- data/lib/bolt_server/schemas/action-run_command.json +12 -0
- data/lib/bolt_server/schemas/action-run_script.json +47 -0
- data/lib/bolt_server/schemas/action-run_task.json +20 -0
- data/lib/bolt_server/schemas/action-upload_file.json +47 -0
- data/lib/bolt_server/schemas/partials/target-any.json +10 -0
- data/lib/bolt_server/schemas/partials/target-ssh.json +88 -0
- data/lib/bolt_server/schemas/partials/target-winrm.json +67 -0
- data/lib/bolt_server/schemas/partials/task.json +94 -0
- data/lib/bolt_server/schemas/transport-ssh.json +25 -0
- data/lib/bolt_server/schemas/transport-winrm.json +19 -0
- data/lib/bolt_server/transport_app.rb +554 -0
- data/lib/bolt_spec/bolt_context.rb +226 -0
- data/lib/bolt_spec/plans/action_stubs/command_stub.rb +51 -0
- data/lib/bolt_spec/plans/action_stubs/download_stub.rb +66 -0
- data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
- data/lib/bolt_spec/plans/action_stubs/script_stub.rb +59 -0
- data/lib/bolt_spec/plans/action_stubs/task_stub.rb +57 -0
- data/lib/bolt_spec/plans/action_stubs/upload_stub.rb +65 -0
- data/lib/bolt_spec/plans/action_stubs.rb +196 -0
- data/lib/bolt_spec/plans/mock_executor.rb +361 -0
- data/lib/bolt_spec/plans/publish_stub.rb +49 -0
- data/lib/bolt_spec/plans.rb +190 -0
- data/lib/bolt_spec/run.rb +246 -0
- data/lib/logging_extensions/logging.rb +13 -0
- data/libexec/apply_catalog.rb +130 -0
- data/libexec/bolt_catalog +68 -0
- data/libexec/custom_facts.rb +63 -0
- data/libexec/query_resources.rb +75 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +21 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +22 -0
- data/modules/aggregate/lib/puppet/functions/aggregate/targets.rb +21 -0
- data/modules/aggregate/plans/count.pp +56 -0
- data/modules/aggregate/plans/targets.pp +56 -0
- data/modules/canary/lib/puppet/functions/canary/merge.rb +13 -0
- data/modules/canary/lib/puppet/functions/canary/random_split.rb +22 -0
- data/modules/canary/lib/puppet/functions/canary/skip.rb +25 -0
- data/modules/canary/plans/init.pp +100 -0
- data/modules/puppet_connect/plans/test_input_data.pp +94 -0
- data/modules/puppetdb_fact/plans/init.pp +20 -0
- data/resources/bolt_bash_completion.sh +214 -0
- metadata +735 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
require 'open3'
|
6
|
+
require 'set'
|
7
|
+
|
8
|
+
require_relative '../../../bolt/error'
|
9
|
+
require_relative '../../../bolt/logger'
|
10
|
+
require_relative '../../../bolt/module_installer/specs/id/gitclone'
|
11
|
+
require_relative '../../../bolt/module_installer/specs/id/github'
|
12
|
+
require_relative '../../../bolt/module_installer/specs/id/gitlab'
|
13
|
+
|
14
|
+
# This class represents a Git module specification.
|
15
|
+
#
|
16
|
+
module Bolt
|
17
|
+
class ModuleInstaller
|
18
|
+
class Specs
|
19
|
+
class GitSpec
|
20
|
+
NAME_REGEX = %r{\A(?:[a-zA-Z0-9]+[-/])?(?<name>[a-z][a-z0-9_]*)\z}.freeze
|
21
|
+
REQUIRED_KEYS = Set.new(%w[git ref]).freeze
|
22
|
+
KNOWN_KEYS = Set.new(%w[git name ref resolve]).freeze
|
23
|
+
|
24
|
+
attr_reader :git, :ref, :resolve, :type
|
25
|
+
|
26
|
+
def initialize(init_hash, config = {})
|
27
|
+
@logger = Bolt::Logger.logger(self)
|
28
|
+
@resolve = init_hash.key?('resolve') ? init_hash['resolve'] : true
|
29
|
+
@git = init_hash['git']
|
30
|
+
@ref = init_hash['ref']
|
31
|
+
@name = parse_name(init_hash['name'])
|
32
|
+
@proxy = config.dig('proxy')
|
33
|
+
@type = :git
|
34
|
+
|
35
|
+
unless @resolve == true || @resolve == false
|
36
|
+
raise Bolt::ValidationError,
|
37
|
+
"Option 'resolve' for module spec #{@git} must be a Boolean"
|
38
|
+
end
|
39
|
+
|
40
|
+
if @name.nil? && @resolve == false
|
41
|
+
raise Bolt::ValidationError,
|
42
|
+
"Missing name for Git module specification: #{@git}. Git module specifications "\
|
43
|
+
"must include a 'name' key when 'resolve' is false."
|
44
|
+
end
|
45
|
+
|
46
|
+
unless valid_url?(@git)
|
47
|
+
raise Bolt::ValidationError,
|
48
|
+
"Invalid URI #{@git}. Valid URIs must begin with 'git@', 'http://', 'https://' or 'ssh://'."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.implements?(hash)
|
53
|
+
KNOWN_KEYS.superset?(hash.keys.to_set) && REQUIRED_KEYS.subset?(hash.keys.to_set)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Parses the name into owner and name segments, and formats the full
|
57
|
+
# name.
|
58
|
+
#
|
59
|
+
private def parse_name(name)
|
60
|
+
return unless name
|
61
|
+
|
62
|
+
unless (match = name.match(NAME_REGEX))
|
63
|
+
raise Bolt::ValidationError,
|
64
|
+
"Invalid name for Git module specification: #{name}. Name must match "\
|
65
|
+
"'name' or 'owner/name'. Owner segment can only include letters or digits. "\
|
66
|
+
"Name segment must start with a lowercase letter and can only include "\
|
67
|
+
"lowercase letters, digits, and underscores."
|
68
|
+
end
|
69
|
+
|
70
|
+
match[:name]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns true if the specification is satisfied by the module.
|
74
|
+
#
|
75
|
+
def satisfied_by?(mod)
|
76
|
+
@type == mod.type && @git == mod.git
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns a hash matching the module spec in bolt-project.yaml
|
80
|
+
#
|
81
|
+
def to_hash
|
82
|
+
{
|
83
|
+
'git' => @git,
|
84
|
+
'ref' => @ref
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns a PuppetfileResolver::Model::GitModule object for resolving.
|
89
|
+
#
|
90
|
+
def to_resolver_module
|
91
|
+
require 'puppetfile-resolver'
|
92
|
+
|
93
|
+
PuppetfileResolver::Puppetfile::GitModule.new(name).tap do |mod|
|
94
|
+
mod.remote = @git
|
95
|
+
mod.ref = sha
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the module's name.
|
100
|
+
#
|
101
|
+
def name
|
102
|
+
@name ||= parse_name(id.name)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the SHA for the module's ref.
|
106
|
+
#
|
107
|
+
def sha
|
108
|
+
id.sha
|
109
|
+
end
|
110
|
+
|
111
|
+
# Gets the ID for the module based on the specified ref and git URL.
|
112
|
+
# This is lazily resolved since Bolt does not always need this information,
|
113
|
+
# and requesting it is expensive.
|
114
|
+
#
|
115
|
+
private def id
|
116
|
+
@id ||= begin
|
117
|
+
# The request methods here return an ID object if the module name and SHA
|
118
|
+
# were found and nil otherwise. This lets Bolt try multiple methods for
|
119
|
+
# finding the module name and SHA, and short circuiting as soon as it does.
|
120
|
+
module_id = Bolt::ModuleInstaller::Specs::ID::GitHub.request(@git, @ref, @proxy) ||
|
121
|
+
Bolt::ModuleInstaller::Specs::ID::GitLab.request(@git, @ref, @proxy) ||
|
122
|
+
Bolt::ModuleInstaller::Specs::ID::GitClone.request(@git, @ref, @proxy)
|
123
|
+
|
124
|
+
unless module_id
|
125
|
+
raise Bolt::Error.new(
|
126
|
+
"Unable to locate metadata and calculate SHA for ref #{@ref} at #{@git}. This may "\
|
127
|
+
"not be a valid module. For more information about how Bolt attempted to locate "\
|
128
|
+
"this information, check the debugging logs.",
|
129
|
+
'bolt/missing-module-metadata-error'
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
module_id
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns true if the URL is valid.
|
138
|
+
#
|
139
|
+
private def valid_url?(url)
|
140
|
+
return true if url.start_with?('git@')
|
141
|
+
|
142
|
+
uri = URI.parse(url)
|
143
|
+
(uri.is_a?(URI::HTTP) || uri.scheme == "ssh") && uri.host
|
144
|
+
rescue URI::InvalidURIError
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
require_relative '../../../../bolt/error'
|
7
|
+
require_relative '../../../../bolt/logger'
|
8
|
+
|
9
|
+
module Bolt
|
10
|
+
class ModuleInstaller
|
11
|
+
class Specs
|
12
|
+
class ID
|
13
|
+
class Base
|
14
|
+
attr_reader :name, :sha
|
15
|
+
|
16
|
+
# @param name [String] The module's name.
|
17
|
+
# @param sha [String] The ref's SHA1.
|
18
|
+
#
|
19
|
+
def initialize(name, sha)
|
20
|
+
@name = name
|
21
|
+
@sha = sha
|
22
|
+
end
|
23
|
+
|
24
|
+
# Request the name and SHA for a module and ref.
|
25
|
+
# This method must return either an ID object or nil. The GitSpec
|
26
|
+
# class relies on this class to return an ID object to indicate
|
27
|
+
# the module was found, or nil to indicate that it should try to
|
28
|
+
# find it another way (such as cloning the repo).
|
29
|
+
#
|
30
|
+
# @param git [String] The URL to the git repo.
|
31
|
+
# @param ref [String] The ref to checkout.
|
32
|
+
# @param proxy [String] A proxy to use when making requests.
|
33
|
+
#
|
34
|
+
def self.request(git, ref, proxy)
|
35
|
+
name, sha = name_and_sha(git, ref, proxy)
|
36
|
+
name && sha ? new(name, sha) : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Stub method for retrieving the module's name and SHA. Must
|
40
|
+
# be implemented by all sub classes.
|
41
|
+
#
|
42
|
+
private_class_method def self.name_and_sha(_git, _ref, _proxy)
|
43
|
+
raise NotImplementedError, 'Class does not implemented #name_and_sha'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Makes a HTTP request.
|
47
|
+
#
|
48
|
+
# @param url [String] The URL to make the request to.
|
49
|
+
# @param proxy [String] A proxy to use when making the request.
|
50
|
+
# @param headers [Hash] Headers to send with the request.
|
51
|
+
#
|
52
|
+
private_class_method def self.make_request(url, proxy, headers = {})
|
53
|
+
uri = URI.parse(url)
|
54
|
+
opts = { use_ssl: uri.scheme == 'https' }
|
55
|
+
args = [uri.host, uri.port]
|
56
|
+
|
57
|
+
if proxy
|
58
|
+
proxy = URI.parse(proxy)
|
59
|
+
args += [proxy.host, proxy.port, proxy.user, proxy.password]
|
60
|
+
end
|
61
|
+
|
62
|
+
Bolt::Logger.debug("Making request to #{loc(url, proxy)}")
|
63
|
+
|
64
|
+
Net::HTTP.start(*args, opts) do |client|
|
65
|
+
client.request(Net::HTTP::Get.new(uri, headers))
|
66
|
+
end
|
67
|
+
rescue StandardError => e
|
68
|
+
raise Bolt::Error.new(
|
69
|
+
"Failed to connect to #{loc(uri, proxy)}: #{e.message}",
|
70
|
+
"bolt/http-connect-error"
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a formatted string describing the URL and proxy used when making
|
75
|
+
# a request.
|
76
|
+
#
|
77
|
+
# @param url [String, URI::HTTP] The URL used.
|
78
|
+
# @param proxy [String, URI::HTTP] The proxy used.
|
79
|
+
#
|
80
|
+
private_class_method def self.loc(url, proxy)
|
81
|
+
proxy ? "#{url} with proxy #{proxy}" : url.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parses the metadata and validates that it is a Hash.
|
85
|
+
#
|
86
|
+
# @param metadata [String] The JSON data to parse.
|
87
|
+
#
|
88
|
+
private_class_method def self.parse_name_from_metadata(metadata)
|
89
|
+
metadata = JSON.parse(metadata)
|
90
|
+
|
91
|
+
unless metadata.is_a?(Hash)
|
92
|
+
raise Bolt::Error.new(
|
93
|
+
"Invalid metadata. Expected a Hash, got a #{metadata.class}: #{metadata}",
|
94
|
+
"bolt/invalid-module-metadata-error"
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
unless metadata.key?('name')
|
99
|
+
raise Bolt::Error.new(
|
100
|
+
"Invalid metadata. Metadata must include a 'name' key.",
|
101
|
+
"bolt/missing-module-name-error"
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
metadata['name']
|
106
|
+
rescue JSON::ParserError => e
|
107
|
+
raise Bolt::Error.new(
|
108
|
+
"Unable to parse metadata as JSON: #{e.message}",
|
109
|
+
"bolt/metadata-parse-error"
|
110
|
+
)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../../../bolt/module_installer/specs/id/base'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class ModuleInstaller
|
7
|
+
class Specs
|
8
|
+
class ID
|
9
|
+
class GitClone < Base
|
10
|
+
# Returns the name and SHA for the module at the given ref.
|
11
|
+
#
|
12
|
+
# @param git [String] The URL to the git repo.
|
13
|
+
# @param ref [String] The ref to checkout.
|
14
|
+
# @param proxy [String] The proxy to use when cloning.
|
15
|
+
#
|
16
|
+
private_class_method def self.name_and_sha(git, ref, proxy)
|
17
|
+
require 'open3'
|
18
|
+
|
19
|
+
unless git?
|
20
|
+
Bolt::Logger.debug("'git' executable not found, unable to use git clone resolution.")
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Clone the repo into a temp directory that will be automatically cleaned up.
|
25
|
+
Dir.mktmpdir do |dir|
|
26
|
+
return nil unless clone_repo(git, ref, dir, proxy)
|
27
|
+
|
28
|
+
# Extract the name from the metadata file and calculate the SHA.
|
29
|
+
Dir.chdir(dir) do
|
30
|
+
[request_name(git, ref), request_sha(git, ref)]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Requests a module's metadata and returns the name from it.
|
36
|
+
#
|
37
|
+
# @param git [String] The URL to the git repo.
|
38
|
+
# @param ref [String] The ref to checkout.
|
39
|
+
#
|
40
|
+
private_class_method def self.request_name(git, ref)
|
41
|
+
command = %W[git show #{ref}:metadata.json]
|
42
|
+
Bolt::Logger.debug("Executing command '#{command.join(' ')}'")
|
43
|
+
|
44
|
+
out, err, status = Open3.capture3(*command)
|
45
|
+
|
46
|
+
unless status.success?
|
47
|
+
raise Bolt::Error.new(
|
48
|
+
"Unable to find metadata file at #{git}: #{err}",
|
49
|
+
"bolt/missing-metadata-file-error"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
Bolt::Logger.debug("Found metadata file at #{git}")
|
54
|
+
parse_name_from_metadata(out)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Requests the SHA for the specified ref.
|
58
|
+
#
|
59
|
+
# @param git [String] The URL to the git repo.
|
60
|
+
# @param ref [String] The ref to checkout.
|
61
|
+
#
|
62
|
+
private_class_method def self.request_sha(git, ref)
|
63
|
+
command = %W[git rev-parse #{ref}^{commit}]
|
64
|
+
Bolt::Logger.debug("Executing command '#{command.join(' ')}'")
|
65
|
+
|
66
|
+
out, err, status = Open3.capture3(*command)
|
67
|
+
|
68
|
+
if status.success?
|
69
|
+
out.strip
|
70
|
+
else
|
71
|
+
raise Bolt::Error.new(
|
72
|
+
"Unable to calculate SHA for ref #{ref} at #{git}: #{err}",
|
73
|
+
"bolt/invalid-ref-error"
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Clones the repository. First attempts to clone a bare repository
|
79
|
+
# and falls back to cloning the full repository if that fails. Cloning
|
80
|
+
# a bare repository is significantly faster for large modules, but
|
81
|
+
# cloning a bare repository using a commit is not supported.
|
82
|
+
#
|
83
|
+
# @param git [String] The URL to the git repo.
|
84
|
+
# @param ref [String] The ref to checkout.
|
85
|
+
# @param dir [String] The directory to clone the repo to.
|
86
|
+
# @param proxy [String] The proxy to use when cloning.
|
87
|
+
#
|
88
|
+
private_class_method def self.clone_repo(git, ref, dir, proxy)
|
89
|
+
clone = %W[git clone #{git} #{dir}]
|
90
|
+
clone += %W[--config "http.proxy=#{proxy}" --config "https.proxy=#{proxy}"] if proxy
|
91
|
+
|
92
|
+
bare_clone = clone + %w[--bare --depth=1]
|
93
|
+
bare_clone.push("--branch=#{ref}") unless ref == 'HEAD'
|
94
|
+
|
95
|
+
# Attempt to clone a bare repository
|
96
|
+
Bolt::Logger.debug("Executing command '#{bare_clone.join(' ')}'")
|
97
|
+
_out, err, status = Open3.capture3(*bare_clone)
|
98
|
+
return true if status.success?
|
99
|
+
Bolt::Logger.debug("Unable to clone bare repository at #{loc(git, proxy)}: #{err}")
|
100
|
+
|
101
|
+
# Fall back to cloning the full repository
|
102
|
+
Bolt::Logger.debug("Executing command '#{clone.join(' ')}'")
|
103
|
+
_out, err, status = Open3.capture3(*clone)
|
104
|
+
Bolt::Logger.debug("Unable to clone repository at #{loc(git, proxy)}: #{err}") unless status.success?
|
105
|
+
status.success?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns true if the 'git' executable is available.
|
109
|
+
#
|
110
|
+
private_class_method def self.git?
|
111
|
+
Open3.capture3('git', '--version')
|
112
|
+
true
|
113
|
+
rescue Errno::ENOENT
|
114
|
+
false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../../../bolt/module_installer/specs/id/base'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class ModuleInstaller
|
7
|
+
class Specs
|
8
|
+
class ID
|
9
|
+
class GitHub < Base
|
10
|
+
# Returns the name and SHA for the module at the given ref.
|
11
|
+
#
|
12
|
+
# @param git [String] The URL to the git repo.
|
13
|
+
# @param ref [String] The ref to use.
|
14
|
+
# @param proxy [String] The proxy to use when making requests.
|
15
|
+
#
|
16
|
+
private_class_method def self.name_and_sha(git, ref, proxy)
|
17
|
+
repo = parse_repo(git)
|
18
|
+
return nil unless repo
|
19
|
+
[request_name(repo, ref, proxy), request_sha(repo, ref, proxy)]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parses the repo path out of the URL.
|
23
|
+
#
|
24
|
+
# @param git [String] The URL to the git repo.
|
25
|
+
#
|
26
|
+
private_class_method def self.parse_repo(git)
|
27
|
+
if git.start_with?('git@github.com:')
|
28
|
+
git.split('git@github.com:').last.split('.git').first
|
29
|
+
elsif git.start_with?('https://github.com')
|
30
|
+
git.split('https://github.com/').last.split('.git').first
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Requests a module's metadata and returns the name from it.
|
35
|
+
#
|
36
|
+
# @param repo [String] The repo ID, i.e. 'owner/repo'
|
37
|
+
# @param ref [String] The ref to use.
|
38
|
+
# @param proxy [String] The proxy to use when making requests.
|
39
|
+
#
|
40
|
+
private_class_method def self.request_name(repo, ref, proxy)
|
41
|
+
metadata_url = "https://raw.githubusercontent.com/#{repo}/#{ref}/metadata.json"
|
42
|
+
response = make_request(metadata_url, proxy)
|
43
|
+
|
44
|
+
case response
|
45
|
+
when Net::HTTPOK
|
46
|
+
Bolt::Logger.debug("Found metadata file at #{loc(metadata_url, proxy)}")
|
47
|
+
parse_name_from_metadata(response.body)
|
48
|
+
else
|
49
|
+
Bolt::Logger.debug("Unable to find metadata file at #{loc(metadata_url, proxy)}")
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Requests the SHA for the specified ref.
|
55
|
+
#
|
56
|
+
# @param repo [String] The repo ID, i.e. 'owner/repo'
|
57
|
+
# @param ref [String] The ref to resolve.
|
58
|
+
# @param proxy [String] The proxy to use when making requests.
|
59
|
+
#
|
60
|
+
private_class_method def self.request_sha(repo, ref, proxy)
|
61
|
+
url = "https://api.github.com/repos/#{repo}/commits/#{ref}"
|
62
|
+
headers = ENV['GITHUB_TOKEN'] ? { "Authorization" => "token #{ENV['GITHUB_TOKEN']}" } : {}
|
63
|
+
response = make_request(url, proxy, headers)
|
64
|
+
|
65
|
+
case response
|
66
|
+
when Net::HTTPOK
|
67
|
+
JSON.parse(response.body).fetch('sha', nil)
|
68
|
+
when Net::HTTPUnauthorized
|
69
|
+
Bolt::Logger.debug("Invalid token at GITHUB_TOKEN, unable to calculate SHA.")
|
70
|
+
nil
|
71
|
+
when Net::HTTPForbidden
|
72
|
+
message = "GitHub API rate limit exceeded, unable to calculate SHA."
|
73
|
+
|
74
|
+
unless ENV['GITHUB_TOKEN']
|
75
|
+
message += " To increase your rate limit, set the GITHUB_TOKEN environment "\
|
76
|
+
"variable with a GitHub personal access token."
|
77
|
+
end
|
78
|
+
|
79
|
+
Bolt::Logger.debug(message)
|
80
|
+
nil
|
81
|
+
else
|
82
|
+
Bolt::Logger.debug("Unable to calculate SHA for ref #{ref}")
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../../../bolt/module_installer/specs/id/base'
|
4
|
+
|
5
|
+
module Bolt
|
6
|
+
class ModuleInstaller
|
7
|
+
class Specs
|
8
|
+
class ID
|
9
|
+
class GitLab < Base
|
10
|
+
# Returns the name and SHA for the module at the given ref.
|
11
|
+
#
|
12
|
+
# @param git [String] The URL to the git repo.
|
13
|
+
# @param ref [String] The ref to use.
|
14
|
+
# @param proxy [String] The proxy to use when making requests.
|
15
|
+
#
|
16
|
+
private_class_method def self.name_and_sha(git, ref, proxy)
|
17
|
+
repo = parse_repo(git)
|
18
|
+
return nil unless repo
|
19
|
+
[request_name(repo, ref, proxy), request_sha(repo, ref, proxy)]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parses the repo path out of the URL.
|
23
|
+
#
|
24
|
+
# @param git [String] The URL to the git repo.
|
25
|
+
#
|
26
|
+
private_class_method def self.parse_repo(git)
|
27
|
+
if git.start_with?('git@gitlab.com:')
|
28
|
+
git.split('git@gitlab.com:').last.split('.git').first
|
29
|
+
elsif git.start_with?('https://gitlab.com')
|
30
|
+
git.split('https://gitlab.com/').last.split('.git').first
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Requests a module's metadata and returns the name from it.
|
35
|
+
#
|
36
|
+
# @param repo [String] The repo ID, i.e. 'owner/repo'
|
37
|
+
# @param ref [String] The ref to use.
|
38
|
+
# @param proxy [String] The proxy to use when making requests.
|
39
|
+
#
|
40
|
+
private_class_method def self.request_name(repo, ref, proxy)
|
41
|
+
metadata_url = "https://gitlab.com/#{repo}/-/raw/#{ref}/metadata.json"
|
42
|
+
response = make_request(metadata_url, proxy)
|
43
|
+
|
44
|
+
case response
|
45
|
+
when Net::HTTPOK
|
46
|
+
Bolt::Logger.debug("Found metadata file at #{loc(metadata_url, proxy)}")
|
47
|
+
parse_name_from_metadata(response.body)
|
48
|
+
else
|
49
|
+
Bolt::Logger.debug("Unable to find metadata file at #{loc(metadata_url, proxy)}")
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Requests the SHA for the specified ref.
|
55
|
+
#
|
56
|
+
# @param repo [String] The repo ID, i.e. 'owner/repo'
|
57
|
+
# @param ref [String] The ref to resolve.
|
58
|
+
# @param proxy [String] The proxy to use when making requests.
|
59
|
+
#
|
60
|
+
private_class_method def self.request_sha(repo, ref, proxy)
|
61
|
+
require 'cgi'
|
62
|
+
|
63
|
+
url = "https://gitlab.com/api/v4/projects/#{CGI.escape(repo)}/repository/commits/#{ref}"
|
64
|
+
headers = ENV['GITLAB_TOKEN'] ? { "PRIVATE-TOKEN" => ENV['GITLAB_TOKEN'] } : {}
|
65
|
+
response = make_request(url, proxy, headers)
|
66
|
+
|
67
|
+
case response
|
68
|
+
when Net::HTTPOK
|
69
|
+
JSON.parse(response.body).fetch('id', nil)
|
70
|
+
when Net::HTTPUnauthorized
|
71
|
+
Bolt::Logger.debug("Invalid token at GITLAB_TOKEN, unable to calculate SHA.")
|
72
|
+
nil
|
73
|
+
when Net::HTTPForbidden
|
74
|
+
message = "GitLab API rate limit exceeded, unable to calculate SHA."
|
75
|
+
|
76
|
+
unless ENV['GITLAB_TOKEN']
|
77
|
+
message += " To increase your rate limit, set the GITLAB_TOKEN environment "\
|
78
|
+
"variable with a GitLab personal access token."
|
79
|
+
end
|
80
|
+
|
81
|
+
Bolt::Logger.debug(message)
|
82
|
+
nil
|
83
|
+
else
|
84
|
+
Bolt::Logger.debug("Unable to calculate SHA for ref #{ref}")
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../bolt/error'
|
4
|
+
require_relative 'specs/forge_spec'
|
5
|
+
require_relative 'specs/git_spec'
|
6
|
+
|
7
|
+
module Bolt
|
8
|
+
class ModuleInstaller
|
9
|
+
class Specs
|
10
|
+
def initialize(specs = [], config = {})
|
11
|
+
@specs = []
|
12
|
+
@config = config
|
13
|
+
|
14
|
+
add_specs(specs)
|
15
|
+
assert_unique_names
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates a list of specs from the modules in a Puppetfile object.
|
19
|
+
#
|
20
|
+
def self.from_puppetfile(puppetfile)
|
21
|
+
new(puppetfile.modules.map(&:to_hash))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a list of specs.
|
25
|
+
#
|
26
|
+
def specs
|
27
|
+
@specs.uniq(&:name)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns true if the specs includes the given name.
|
31
|
+
#
|
32
|
+
def include?(name)
|
33
|
+
_owner, name = name.tr('-', '/').split('/', 2)
|
34
|
+
@specs.any? { |spec| spec.name == name }
|
35
|
+
end
|
36
|
+
|
37
|
+
# Adds a spec.
|
38
|
+
#
|
39
|
+
def add_specs(*specs)
|
40
|
+
specs.flatten.map do |spec|
|
41
|
+
case spec
|
42
|
+
when Hash
|
43
|
+
@specs.unshift spec_from_hash(spec)
|
44
|
+
else
|
45
|
+
@specs.unshift spec
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Parses a spec hash into a spec object.
|
51
|
+
#
|
52
|
+
private def spec_from_hash(hash)
|
53
|
+
return ForgeSpec.new(hash) if ForgeSpec.implements?(hash)
|
54
|
+
return GitSpec.new(hash, @config) if GitSpec.implements?(hash)
|
55
|
+
|
56
|
+
raise Bolt::ValidationError, <<~MESSAGE.chomp
|
57
|
+
Invalid module specification:
|
58
|
+
#{hash.to_yaml.lines.drop(1).join.chomp}
|
59
|
+
|
60
|
+
To read more about specifying modules, see https://pup.pt/bolt-module-specs
|
61
|
+
MESSAGE
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if all specs are satisfied by the modules in a Puppetfile.
|
65
|
+
#
|
66
|
+
def satisfied_by?(puppetfile)
|
67
|
+
@specs.all? do |spec|
|
68
|
+
puppetfile.modules.any? do |mod|
|
69
|
+
spec.satisfied_by?(mod)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Asserts that all specs are unique by name. The puppetfile-resolver
|
75
|
+
# library also does this, but the error it raises isn't as helpful.
|
76
|
+
#
|
77
|
+
private def assert_unique_names
|
78
|
+
duplicates = @specs.group_by(&:name).select { |_name, specs| specs.count > 1 }
|
79
|
+
|
80
|
+
if duplicates.any?
|
81
|
+
message = String.new
|
82
|
+
|
83
|
+
duplicates.each do |name, duplicate_specs|
|
84
|
+
message << <<~MESSAGE
|
85
|
+
Detected multiple module specifications with name #{name}:
|
86
|
+
#{duplicate_specs.map(&:to_hash).to_yaml.lines.drop(1).join}
|
87
|
+
MESSAGE
|
88
|
+
end
|
89
|
+
|
90
|
+
raise Bolt::Error.new(message.chomp, "bolt/duplicate-spec-name-error")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|