bolt 2.40.2 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +19 -17
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +25 -0
- data/bolt-modules/boltlib/lib/puppet/functions/parallelize.rb +6 -8
- data/bolt-modules/boltlib/lib/puppet/functions/wait_until_available.rb +7 -3
- data/lib/bolt/analytics.rb +3 -2
- data/lib/bolt/applicator.rb +11 -1
- data/lib/bolt/bolt_option_parser.rb +3 -113
- data/lib/bolt/catalog.rb +10 -29
- data/lib/bolt/cli.rb +54 -155
- data/lib/bolt/config.rb +62 -239
- data/lib/bolt/config/options.rb +58 -97
- data/lib/bolt/config/transport/local.rb +1 -0
- data/lib/bolt/config/transport/options.rb +8 -1
- data/lib/bolt/config/transport/orch.rb +1 -0
- data/lib/bolt/executor.rb +15 -5
- data/lib/bolt/inventory.rb +3 -2
- data/lib/bolt/inventory/group.rb +35 -4
- data/lib/bolt/inventory/inventory.rb +1 -1
- data/lib/bolt/logger.rb +115 -11
- data/lib/bolt/module.rb +10 -2
- data/lib/bolt/module_installer.rb +4 -2
- data/lib/bolt/module_installer/resolver.rb +65 -12
- data/lib/bolt/module_installer/specs/forge_spec.rb +8 -2
- data/lib/bolt/module_installer/specs/git_spec.rb +17 -2
- data/lib/bolt/outputter/human.rb +9 -5
- data/lib/bolt/outputter/json.rb +16 -16
- data/lib/bolt/outputter/rainbow.rb +3 -3
- data/lib/bolt/pal.rb +94 -14
- data/lib/bolt/pal/yaml_plan.rb +8 -2
- data/lib/bolt/pal/yaml_plan/evaluator.rb +7 -19
- data/lib/bolt/pal/yaml_plan/step.rb +3 -24
- data/lib/bolt/pal/yaml_plan/step/upload.rb +2 -2
- data/lib/bolt/pal/yaml_plan/transpiler.rb +6 -1
- data/lib/bolt/plugin.rb +3 -3
- data/lib/bolt/plugin/cache.rb +7 -7
- data/lib/bolt/plugin/module.rb +0 -23
- data/lib/bolt/plugin/puppet_connect_data.rb +77 -0
- data/lib/bolt/plugin/puppetdb.rb +1 -1
- data/lib/bolt/project.rb +54 -81
- data/lib/bolt/project_manager.rb +4 -3
- data/lib/bolt/project_manager/module_migrator.rb +6 -5
- data/lib/bolt/rerun.rb +1 -1
- data/lib/bolt/result.rb +6 -1
- data/lib/bolt/shell/bash.rb +9 -4
- data/lib/bolt/shell/bash/tmpdir.rb +4 -1
- data/lib/bolt/shell/powershell.rb +9 -5
- data/lib/bolt/shell/powershell/snippets.rb +37 -150
- data/lib/bolt/task.rb +1 -1
- data/lib/bolt/transport/base.rb +0 -9
- data/lib/bolt/transport/docker.rb +1 -125
- data/lib/bolt/transport/docker/connection.rb +86 -161
- data/lib/bolt/transport/local.rb +1 -9
- data/lib/bolt/transport/orch/connection.rb +1 -1
- data/lib/bolt/transport/ssh.rb +1 -2
- data/lib/bolt/transport/ssh/connection.rb +1 -1
- data/lib/bolt/validator.rb +2 -2
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_server/config.rb +1 -1
- data/lib/bolt_server/transport_app.rb +48 -31
- data/lib/bolt_spec/bolt_context.rb +9 -4
- data/lib/bolt_spec/plans.rb +1 -109
- data/libexec/bolt_catalog +1 -1
- data/modules/aggregate/plans/count.pp +21 -0
- data/modules/aggregate/plans/targets.pp +21 -0
- data/modules/puppet_connect/plans/test_input_data.pp +67 -0
- data/modules/puppetdb_fact/plans/init.pp +10 -0
- metadata +28 -19
- data/modules/aggregate/plans/nodes.pp +0 -36
@@ -48,7 +48,10 @@ module Bolt
|
|
48
48
|
def delete
|
49
49
|
result = @shell.execute(['rm', '-rf', @path], sudoable: true, run_as: @owner)
|
50
50
|
if result.exit_code != 0
|
51
|
-
|
51
|
+
Bolt::Logger.warn(
|
52
|
+
"fail_cleanup",
|
53
|
+
"Failed to clean up tmpdir '#{@path}': #{result.stderr.string}"
|
54
|
+
)
|
52
55
|
end
|
53
56
|
# For testing
|
54
57
|
result.stderr.string
|
@@ -23,11 +23,10 @@ module Bolt
|
|
23
23
|
# This lets us know how many targets have Powershell 2, and lets the
|
24
24
|
# user know how many targets they have with PS2
|
25
25
|
msg = "Detected PowerShell 2 on one or more targets.\nPowerShell 2 "\
|
26
|
-
"is
|
27
|
-
"bolt-debug.log or run with '--log-level debug' to see the full "\
|
26
|
+
"is unsupported. See bolt-debug.log or run with '--log-level debug' to see the full "\
|
28
27
|
"list of targets with PowerShell 2."
|
29
28
|
|
30
|
-
Bolt::Logger.
|
29
|
+
Bolt::Logger.deprecate_once("powershell_2", msg)
|
31
30
|
@logger.debug("Detected PowerShell 2 on #{target}.")
|
32
31
|
end
|
33
32
|
end
|
@@ -163,7 +162,7 @@ module Bolt
|
|
163
162
|
if target.options['cleanup']
|
164
163
|
rmdir(@tmpdir)
|
165
164
|
else
|
166
|
-
|
165
|
+
Bolt::Logger.warn("Skipping cleanup of tmpdir '#{@tmpdir}'", "skip_cleanup")
|
167
166
|
end
|
168
167
|
end
|
169
168
|
end
|
@@ -275,7 +274,12 @@ module Bolt
|
|
275
274
|
[]
|
276
275
|
end
|
277
276
|
|
278
|
-
output = execute([
|
277
|
+
output = execute([
|
278
|
+
Snippets.shell_init,
|
279
|
+
Snippets.append_ps_module_path(dir),
|
280
|
+
*env_assignments,
|
281
|
+
command
|
282
|
+
].join("\n"))
|
279
283
|
|
280
284
|
Bolt::Result.for_task(target, output.stdout.string,
|
281
285
|
output.stderr.string,
|
@@ -55,27 +55,59 @@ module Bolt
|
|
55
55
|
}
|
56
56
|
#{build_arg_list}
|
57
57
|
|
58
|
+
switch -regex ( Get-ExecutionPolicy )
|
59
|
+
{
|
60
|
+
'^AllSigned'
|
61
|
+
{
|
62
|
+
if ((Get-AuthenticodeSignature -File "#{script_path}").Status -ne 'Valid') {
|
63
|
+
$Host.UI.WriteErrorLine("Error: Target host Powershell ExecutionPolicy is set to ${_} and script '#{script_path}' does not contain a valid signature.")
|
64
|
+
exit 1;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
'^Restricted'
|
68
|
+
{
|
69
|
+
$Host.UI.WriteErrorLine("Error: Target host Powershell ExecutionPolicy is set to ${_} which denies running any scripts on the target.")
|
70
|
+
exit 1;
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
if([string]::IsNullOrEmpty($invokeArgs.ScriptBlock)){
|
75
|
+
$Host.UI.WriteErrorLine("Error: Failed to obtain scriptblock from '#{script_path}'. Running scripts might be disabled on this system. For more information, see about_Execution_Policies at https:/go.microsoft.com/fwlink/?LinkID=135170");
|
76
|
+
exit 1;
|
77
|
+
}
|
78
|
+
|
58
79
|
try
|
59
80
|
{
|
60
81
|
Invoke-Command @invokeArgs
|
61
82
|
}
|
62
83
|
catch
|
63
84
|
{
|
64
|
-
|
65
|
-
exit 1
|
85
|
+
$Host.UI.WriteErrorLine("[$($_.FullyQualifiedErrorId)] Exception $($_.InvocationInfo.PositionMessage).`n$($_.Exception.Message)");
|
86
|
+
exit 1;
|
66
87
|
}
|
67
88
|
PS
|
68
89
|
end
|
69
90
|
|
91
|
+
def append_ps_module_path(directory)
|
92
|
+
<<~PS
|
93
|
+
$env:PSModulePath += ";#{directory}"
|
94
|
+
PS
|
95
|
+
end
|
96
|
+
|
70
97
|
def ps_task(path, arguments)
|
71
98
|
<<~PS
|
72
99
|
$private:tempArgs = Get-ContentAsJson (
|
73
|
-
|
100
|
+
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{Base64.encode64(JSON.dump(arguments))}'))
|
74
101
|
)
|
75
102
|
$allowedArgs = (Get-Command "#{path}").Parameters.Keys
|
76
103
|
$private:taskArgs = @{}
|
77
104
|
$private:tempArgs.Keys | ? { $allowedArgs -contains $_ } | % { $private:taskArgs[$_] = $private:tempArgs[$_] }
|
78
|
-
try {
|
105
|
+
try {
|
106
|
+
& "#{path}" @taskArgs
|
107
|
+
} catch {
|
108
|
+
$Host.UI.WriteErrorLine("[$($_.FullyQualifiedErrorId)] Exception $($_.InvocationInfo.PositionMessage).`n$($_.Exception.Message)");
|
109
|
+
exit 1;
|
110
|
+
}
|
79
111
|
PS
|
80
112
|
end
|
81
113
|
|
@@ -102,151 +134,11 @@ module Bolt
|
|
102
134
|
"${boltBaseDir}\\hiera\\lib;" +
|
103
135
|
$ENV:RUBYLIB
|
104
136
|
|
105
|
-
Add-Type -AssemblyName System.ServiceModel.Web, System.Runtime.Serialization
|
106
|
-
$utf8 = [System.Text.Encoding]::UTF8
|
107
|
-
|
108
|
-
function Write-Stream {
|
109
|
-
PARAM(
|
110
|
-
[Parameter(Position=0)] $stream,
|
111
|
-
[Parameter(ValueFromPipeline=$true)] $string
|
112
|
-
)
|
113
|
-
PROCESS {
|
114
|
-
$bytes = $utf8.GetBytes($string)
|
115
|
-
$stream.Write( $bytes, 0, $bytes.Length )
|
116
|
-
}
|
117
|
-
}
|
118
|
-
|
119
|
-
function Convert-JsonToXml {
|
120
|
-
PARAM([Parameter(ValueFromPipeline=$true)] [string[]] $json)
|
121
|
-
BEGIN {
|
122
|
-
$mStream = New-Object System.IO.MemoryStream
|
123
|
-
}
|
124
|
-
PROCESS {
|
125
|
-
$json | Write-Stream -Stream $mStream
|
126
|
-
}
|
127
|
-
END {
|
128
|
-
$mStream.Position = 0
|
129
|
-
try {
|
130
|
-
$jsonReader = [System.Runtime.Serialization.Json.JsonReaderWriterFactory]::CreateJsonReader($mStream,[System.Xml.XmlDictionaryReaderQuotas]::Max)
|
131
|
-
$xml = New-Object Xml.XmlDocument
|
132
|
-
$xml.Load($jsonReader)
|
133
|
-
$xml
|
134
|
-
} finally {
|
135
|
-
$jsonReader.Close()
|
136
|
-
$mStream.Dispose()
|
137
|
-
}
|
138
|
-
}
|
139
|
-
}
|
140
|
-
|
141
|
-
Function ConvertFrom-Xml {
|
142
|
-
[CmdletBinding(DefaultParameterSetName="AutoType")]
|
143
|
-
PARAM(
|
144
|
-
[Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [Xml.XmlNode] $xml,
|
145
|
-
[Parameter(Mandatory=$true,ParameterSetName="ManualType")] [Type] $Type,
|
146
|
-
[Switch] $ForceType
|
147
|
-
)
|
148
|
-
PROCESS{
|
149
|
-
if (Get-Member -InputObject $xml -Name root) {
|
150
|
-
return $xml.root.Objects | ConvertFrom-Xml
|
151
|
-
} elseif (Get-Member -InputObject $xml -Name Objects) {
|
152
|
-
return $xml.Objects | ConvertFrom-Xml
|
153
|
-
}
|
154
|
-
$propbag = @{}
|
155
|
-
foreach ($name in Get-Member -InputObject $xml -MemberType Properties | Where-Object{$_.Name -notmatch "^(__.*|type)$"} | Select-Object -ExpandProperty name) {
|
156
|
-
Write-Debug "$Name Type: $($xml.$Name.type)" -Debug:$false
|
157
|
-
$propbag."$Name" = Convert-Properties $xml."$name"
|
158
|
-
}
|
159
|
-
if (!$Type -and $xml.HasAttribute("__type")) { $Type = $xml.__Type }
|
160
|
-
if ($ForceType -and $Type) {
|
161
|
-
try {
|
162
|
-
$output = New-Object $Type -Property $propbag
|
163
|
-
} catch {
|
164
|
-
$output = New-Object PSObject -Property $propbag
|
165
|
-
$output.PsTypeNames.Insert(0, $xml.__type)
|
166
|
-
}
|
167
|
-
} elseif ($propbag.Count -ne 0) {
|
168
|
-
$output = New-Object PSObject -Property $propbag
|
169
|
-
if ($Type) {
|
170
|
-
$output.PsTypeNames.Insert(0, $Type)
|
171
|
-
}
|
172
|
-
}
|
173
|
-
return $output
|
174
|
-
}
|
175
|
-
}
|
176
|
-
|
177
|
-
Function Convert-Properties {
|
178
|
-
PARAM($InputObject)
|
179
|
-
switch ($InputObject.type) {
|
180
|
-
"object" {
|
181
|
-
return (ConvertFrom-Xml -Xml $InputObject)
|
182
|
-
}
|
183
|
-
"string" {
|
184
|
-
$MightBeADate = $InputObject.get_InnerText() -as [DateTime]
|
185
|
-
## Strings that are actually dates (*grumble* JSON is crap)
|
186
|
-
if ($MightBeADate -and $propbag."$Name" -eq $MightBeADate.ToString("G")) {
|
187
|
-
return $MightBeADate
|
188
|
-
} else {
|
189
|
-
return $InputObject.get_InnerText()
|
190
|
-
}
|
191
|
-
}
|
192
|
-
"number" {
|
193
|
-
$number = $InputObject.get_InnerText()
|
194
|
-
if ($number -eq ($number -as [int])) {
|
195
|
-
return $number -as [int]
|
196
|
-
} elseif ($number -eq ($number -as [double])) {
|
197
|
-
return $number -as [double]
|
198
|
-
} else {
|
199
|
-
return $number -as [decimal]
|
200
|
-
}
|
201
|
-
}
|
202
|
-
"boolean" {
|
203
|
-
return [bool]::parse($InputObject.get_InnerText())
|
204
|
-
}
|
205
|
-
"null" {
|
206
|
-
return $null
|
207
|
-
}
|
208
|
-
"array" {
|
209
|
-
[object[]]$Items = $(foreach( $item in $InputObject.GetEnumerator() ) {
|
210
|
-
Convert-Properties $item
|
211
|
-
})
|
212
|
-
return $Items
|
213
|
-
}
|
214
|
-
default {
|
215
|
-
return $InputObject
|
216
|
-
}
|
217
|
-
}
|
218
|
-
}
|
219
|
-
|
220
|
-
Function ConvertFrom-Json2 {
|
221
|
-
[CmdletBinding()]
|
222
|
-
PARAM(
|
223
|
-
[Parameter(ValueFromPipeline=$true,Mandatory=$true,Position=1)] [string] $InputObject,
|
224
|
-
[Parameter(Mandatory=$true)] [Type] $Type,
|
225
|
-
[Switch] $ForceType
|
226
|
-
)
|
227
|
-
PROCESS {
|
228
|
-
$null = $PSBoundParameters.Remove("InputObject")
|
229
|
-
[Xml.XmlElement]$xml = (Convert-JsonToXml $InputObject).Root
|
230
|
-
if ($xml) {
|
231
|
-
if ($xml.Objects) {
|
232
|
-
$xml.Objects.Item.GetEnumerator() | ConvertFrom-Xml @PSBoundParameters
|
233
|
-
} elseif ($xml.Item -and $xml.Item -isnot [System.Management.Automation.PSParameterizedProperty]) {
|
234
|
-
$xml.Item | ConvertFrom-Xml @PSBoundParameters
|
235
|
-
} else {
|
236
|
-
$xml | ConvertFrom-Xml @PSBoundParameters
|
237
|
-
}
|
238
|
-
} else {
|
239
|
-
Write-Error "Failed to parse JSON with JsonReader" -Debug:$false
|
240
|
-
}
|
241
|
-
}
|
242
|
-
}
|
243
|
-
|
244
137
|
function ConvertFrom-PSCustomObject
|
245
138
|
{
|
246
139
|
PARAM([Parameter(ValueFromPipeline = $true)] $InputObject)
|
247
140
|
PROCESS {
|
248
141
|
if ($null -eq $InputObject) { return $null }
|
249
|
-
|
250
142
|
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
|
251
143
|
$collection = @(
|
252
144
|
foreach ($object in $InputObject) { ConvertFrom-PSCustomObject $object }
|
@@ -274,12 +166,7 @@ module Bolt
|
|
274
166
|
[Parameter(Mandatory = $false)] [Text.Encoding] $Encoding = [Text.Encoding]::UTF8
|
275
167
|
)
|
276
168
|
|
277
|
-
|
278
|
-
if ($PSVersionTable.PSVersion -lt [Version]'3.0') {
|
279
|
-
$Text | ConvertFrom-Json2 -Type PSObject | ConvertFrom-PSCustomObject
|
280
|
-
} else {
|
281
|
-
$Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
|
282
|
-
}
|
169
|
+
$Text | ConvertFrom-Json | ConvertFrom-PSCustomObject
|
283
170
|
}
|
284
171
|
PS
|
285
172
|
end
|
data/lib/bolt/task.rb
CHANGED
@@ -149,7 +149,7 @@ module Bolt
|
|
149
149
|
if unknown_keys.any?
|
150
150
|
msg = "Metadata for task '#{@name}' contains unknown keys: #{unknown_keys.join(', ')}."
|
151
151
|
msg += " This could be a typo in the task metadata or may result in incorrect behavior."
|
152
|
-
|
152
|
+
Bolt::Logger.warn("unknown_task_metadata_keys", msg)
|
153
153
|
end
|
154
154
|
end
|
155
155
|
end
|
data/lib/bolt/transport/base.rb
CHANGED
@@ -74,15 +74,6 @@ module Bolt
|
|
74
74
|
interpreters[Pathname(executable).extname] if interpreters
|
75
75
|
end
|
76
76
|
|
77
|
-
# Transform a parameter map to an environment variable map, with parameter names prefixed
|
78
|
-
# with 'PT_' and values transformed to JSON unless they're strings.
|
79
|
-
def envify_params(params)
|
80
|
-
params.each_with_object({}) do |(k, v), h|
|
81
|
-
v = v.to_json unless v.is_a?(String)
|
82
|
-
h["PT_#{k}"] = v
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
77
|
# Raises an error if more than one target was given in the batch.
|
87
78
|
#
|
88
79
|
# The default implementations of batch_* strictly assume the transport is
|
@@ -6,7 +6,7 @@ require 'bolt/transport/base'
|
|
6
6
|
|
7
7
|
module Bolt
|
8
8
|
module Transport
|
9
|
-
class Docker <
|
9
|
+
class Docker < Simple
|
10
10
|
def provided_features
|
11
11
|
['shell']
|
12
12
|
end
|
@@ -16,130 +16,6 @@ module Bolt
|
|
16
16
|
conn.connect
|
17
17
|
yield conn
|
18
18
|
end
|
19
|
-
|
20
|
-
def upload(target, source, destination, _options = {})
|
21
|
-
with_connection(target) do |conn|
|
22
|
-
conn.with_remote_tmpdir do |dir|
|
23
|
-
basename = File.basename(source)
|
24
|
-
tmpfile = "#{dir}/#{basename}"
|
25
|
-
if File.directory?(source)
|
26
|
-
conn.write_remote_directory(source, tmpfile)
|
27
|
-
else
|
28
|
-
conn.write_remote_file(source, tmpfile)
|
29
|
-
end
|
30
|
-
|
31
|
-
_, stderr, exitcode = conn.execute('mv', tmpfile, destination, {})
|
32
|
-
if exitcode != 0
|
33
|
-
message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{stderr}"
|
34
|
-
raise Bolt::Node::FileError.new(message, 'MV_ERROR')
|
35
|
-
end
|
36
|
-
end
|
37
|
-
Bolt::Result.for_upload(target, source, destination)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def download(target, source, destination, _options = {})
|
42
|
-
with_connection(target) do |conn|
|
43
|
-
download = File.join(destination, Bolt::Util.unix_basename(source))
|
44
|
-
conn.download_remote_content(source, destination)
|
45
|
-
Bolt::Result.for_download(target, source, destination, download)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def run_command(target, command, options = {}, position = [])
|
50
|
-
execute_options = {}
|
51
|
-
execute_options[:tty] = target.options['tty']
|
52
|
-
execute_options[:environment] = options[:env_vars]
|
53
|
-
|
54
|
-
if target.options['shell-command'] && !target.options['shell-command'].empty?
|
55
|
-
# escape any double quotes in command
|
56
|
-
command = command.gsub('"', '\"')
|
57
|
-
command = "#{target.options['shell-command']} \" #{command}\""
|
58
|
-
end
|
59
|
-
with_connection(target) do |conn|
|
60
|
-
stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), execute_options)
|
61
|
-
Bolt::Result.for_command(target,
|
62
|
-
stdout,
|
63
|
-
stderr,
|
64
|
-
exitcode,
|
65
|
-
'command',
|
66
|
-
command,
|
67
|
-
position)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def run_script(target, script, arguments, options = {}, position = [])
|
72
|
-
# unpack any Sensitive data
|
73
|
-
arguments = unwrap_sensitive_args(arguments)
|
74
|
-
execute_options = {}
|
75
|
-
execute_options[:environment] = options[:env_vars]
|
76
|
-
|
77
|
-
with_connection(target) do |conn|
|
78
|
-
conn.with_remote_tmpdir do |dir|
|
79
|
-
remote_path = conn.write_remote_executable(dir, script)
|
80
|
-
stdout, stderr, exitcode = conn.execute(remote_path, *arguments, execute_options)
|
81
|
-
Bolt::Result.for_command(target,
|
82
|
-
stdout,
|
83
|
-
stderr,
|
84
|
-
exitcode,
|
85
|
-
'script',
|
86
|
-
script,
|
87
|
-
position)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def run_task(target, task, arguments, _options = {}, position = [])
|
93
|
-
implementation = task.select_implementation(target, provided_features)
|
94
|
-
executable = implementation['path']
|
95
|
-
input_method = implementation['input_method']
|
96
|
-
extra_files = implementation['files']
|
97
|
-
input_method ||= 'both'
|
98
|
-
|
99
|
-
# unpack any Sensitive data
|
100
|
-
arguments = unwrap_sensitive_args(arguments)
|
101
|
-
with_connection(target) do |conn|
|
102
|
-
execute_options = {}
|
103
|
-
execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
|
104
|
-
conn.with_remote_tmpdir do |dir|
|
105
|
-
if extra_files.empty?
|
106
|
-
task_dir = dir
|
107
|
-
else
|
108
|
-
# TODO: optimize upload of directories
|
109
|
-
arguments['_installdir'] = dir
|
110
|
-
task_dir = File.join(dir, task.tasks_dir)
|
111
|
-
conn.mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
|
112
|
-
extra_files.each do |file|
|
113
|
-
conn.write_remote_file(file['path'], File.join(dir, file['name']))
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
remote_task_path = conn.write_remote_executable(task_dir, executable)
|
118
|
-
|
119
|
-
if Bolt::Task::STDIN_METHODS.include?(input_method)
|
120
|
-
execute_options[:stdin] = StringIO.new(JSON.dump(arguments))
|
121
|
-
end
|
122
|
-
|
123
|
-
if Bolt::Task::ENVIRONMENT_METHODS.include?(input_method)
|
124
|
-
execute_options[:environment] = envify_params(arguments)
|
125
|
-
end
|
126
|
-
|
127
|
-
stdout, stderr, exitcode = conn.execute(remote_task_path, execute_options)
|
128
|
-
Bolt::Result.for_task(target,
|
129
|
-
stdout,
|
130
|
-
stderr,
|
131
|
-
exitcode,
|
132
|
-
task.name,
|
133
|
-
position)
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def connected?(target)
|
139
|
-
with_connection(target) { true }
|
140
|
-
rescue Bolt::Node::ConnectError
|
141
|
-
false
|
142
|
-
end
|
143
19
|
end
|
144
20
|
end
|
145
21
|
end
|
@@ -5,227 +5,152 @@ require 'bolt/node/errors'
|
|
5
5
|
|
6
6
|
module Bolt
|
7
7
|
module Transport
|
8
|
-
class Docker <
|
8
|
+
class Docker < Simple
|
9
9
|
class Connection
|
10
|
+
attr_reader :user, :target
|
11
|
+
|
10
12
|
def initialize(target)
|
11
13
|
raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host
|
12
14
|
@target = target
|
15
|
+
@user = ENV['USER'] || Etc.getlogin
|
13
16
|
@logger = Bolt::Logger.logger(target.safe_name)
|
14
|
-
@
|
15
|
-
@
|
17
|
+
@container_info = {}
|
18
|
+
@docker_host = target.options['service-url']
|
19
|
+
@logger.trace("Initializing docker connection to #{target.safe_name}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def shell
|
23
|
+
@shell ||= if Bolt::Util.windows?
|
24
|
+
Bolt::Shell::Powershell.new(target, self)
|
25
|
+
else
|
26
|
+
Bolt::Shell::Bash.new(target, self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# The full ID of the target container
|
31
|
+
#
|
32
|
+
# @return [String] The full ID of the target container
|
33
|
+
def container_id
|
34
|
+
@container_info["Id"]
|
16
35
|
end
|
17
36
|
|
18
37
|
def connect
|
19
38
|
# We don't actually have a connection, but we do need to
|
20
39
|
# check that the container exists and is running.
|
21
|
-
output =
|
22
|
-
index = output.find_index { |item| item["ID"] ==
|
23
|
-
raise "Could not find a container with name or ID matching '#{
|
40
|
+
output = execute_local_json_command('ps')
|
41
|
+
index = output.find_index { |item| item["ID"] == target.host || item["Names"] == target.host }
|
42
|
+
raise "Could not find a container with name or ID matching '#{target.host}'" if index.nil?
|
24
43
|
# Now find the indepth container information
|
25
|
-
output =
|
44
|
+
output = execute_local_json_command('inspect', [output[index]["ID"]])
|
26
45
|
# Store the container information for later
|
27
46
|
@container_info = output[0]
|
28
47
|
@logger.trace { "Opened session" }
|
29
48
|
true
|
30
49
|
rescue StandardError => e
|
31
50
|
raise Bolt::Node::ConnectError.new(
|
32
|
-
"Failed to connect to #{
|
51
|
+
"Failed to connect to #{target.safe_name}: #{e.message}",
|
33
52
|
'CONNECT_ERROR'
|
34
53
|
)
|
35
54
|
end
|
36
55
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# @option opts [String] :interpreter statements that are prefixed to the command e.g `/bin/bash` or `cmd.exe /c`
|
42
|
-
# @option opts [Hash] :environment A hash of environment variables that will be injected into the command
|
43
|
-
# @option opts [IO] :stdin An IO object that will be used to redirect STDIN for the docker command
|
44
|
-
def execute(*command, options)
|
45
|
-
command.unshift(options[:interpreter]) if options[:interpreter]
|
46
|
-
# Build the `--env` parameters
|
47
|
-
envs = []
|
48
|
-
if options[:environment]
|
49
|
-
options[:environment].each { |env, val| envs.concat(['--env', "#{env}=#{val}"]) }
|
56
|
+
def add_env_vars(env_vars)
|
57
|
+
@env_vars = env_vars.each_with_object([]) do |env_var, acc|
|
58
|
+
acc << "--env"
|
59
|
+
acc << "#{env_var[0]}=#{env_var[1]}"
|
50
60
|
end
|
61
|
+
end
|
51
62
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
63
|
+
# Executes a command inside the target container. This is called from the shell class.
|
64
|
+
#
|
65
|
+
# @param command [string] The command to run
|
66
|
+
def execute(command)
|
67
|
+
args = []
|
68
|
+
# CODEREVIEW: Is it always safe to pass --interactive?
|
69
|
+
args += %w[--interactive]
|
70
|
+
args += %w[--tty] if target.options['tty']
|
71
|
+
args += %W[--env DOCKER_HOST=#{@docker_host}] if @docker_host
|
72
|
+
args += @env_vars if @env_vars
|
73
|
+
|
74
|
+
if target.options['shell-command'] && !target.options['shell-command'].empty?
|
75
|
+
# escape any double quotes in command
|
76
|
+
command = command.gsub('"', '\"')
|
77
|
+
command = "#{target.options['shell-command']} \"#{command}\""
|
78
|
+
end
|
61
79
|
|
62
|
-
|
80
|
+
docker_command = %w[docker exec] + args + [container_id] + Shellwords.split(command)
|
81
|
+
@logger.trace { "Executing: #{docker_command.join(' ')}" }
|
63
82
|
|
64
|
-
|
65
|
-
status = status.nil? ? -32768 : status.exitstatus
|
66
|
-
if status == 0
|
67
|
-
@logger.trace { "Command returned successfully" }
|
68
|
-
else
|
69
|
-
@logger.trace { "Command failed with exit code #{status}" }
|
70
|
-
end
|
71
|
-
stdout_str.force_encoding(Encoding::UTF_8)
|
72
|
-
stderr_str.force_encoding(Encoding::UTF_8)
|
73
|
-
# Normalise line endings
|
74
|
-
stdout_str.gsub!("\r\n", "\n")
|
75
|
-
stderr_str.gsub!("\r\n", "\n")
|
76
|
-
[stdout_str, stderr_str, status]
|
83
|
+
Open3.popen3(*docker_command)
|
77
84
|
rescue StandardError
|
78
85
|
@logger.trace { "Command aborted" }
|
79
86
|
raise
|
80
87
|
end
|
81
88
|
|
82
|
-
def
|
83
|
-
@logger.trace { "Uploading #{source} to #{destination}" }
|
84
|
-
_, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
|
85
|
-
unless status.exitstatus.zero?
|
86
|
-
raise "Error writing file to container #{@container_id}: #{stdout_str}"
|
87
|
-
end
|
88
|
-
rescue StandardError => e
|
89
|
-
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
90
|
-
end
|
91
|
-
|
92
|
-
def write_remote_directory(source, destination)
|
89
|
+
def upload_file(source, destination)
|
93
90
|
@logger.trace { "Uploading #{source} to #{destination}" }
|
94
|
-
|
91
|
+
_stdout, stderr, status = execute_local_command('cp', [source, "#{container_id}:#{destination}"])
|
95
92
|
unless status.exitstatus.zero?
|
96
|
-
raise "Error writing
|
93
|
+
raise "Error writing to container #{container_id}: #{stderr}"
|
97
94
|
end
|
98
95
|
rescue StandardError => e
|
99
96
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
100
97
|
end
|
101
98
|
|
102
|
-
def
|
99
|
+
def download_file(source, destination, _download)
|
103
100
|
@logger.trace { "Downloading #{source} to #{destination}" }
|
104
101
|
# Create the destination directory, otherwise copying a source directory with Docker will
|
105
102
|
# copy the *contents* of the directory.
|
106
103
|
# https://docs.docker.com/engine/reference/commandline/cp/
|
107
104
|
FileUtils.mkdir_p(destination)
|
108
|
-
|
105
|
+
_stdout, stderr, status = execute_local_command('cp', ["#{container_id}:#{source}", destination])
|
109
106
|
unless status.exitstatus.zero?
|
110
|
-
raise "Error downloading content from container #{
|
107
|
+
raise "Error downloading content from container #{container_id}: #{stderr}"
|
111
108
|
end
|
112
109
|
rescue StandardError => e
|
113
110
|
raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
|
114
111
|
end
|
115
112
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
if exitcode != 0
|
130
|
-
raise Bolt::Node::FileError.new("Could not make tmpdir: #{stderr}", 'TMPDIR_ERROR')
|
131
|
-
end
|
132
|
-
tmppath || stdout.first
|
133
|
-
end
|
134
|
-
|
135
|
-
def with_remote_tmpdir
|
136
|
-
dir = make_tmpdir
|
137
|
-
yield dir
|
138
|
-
ensure
|
139
|
-
if dir
|
140
|
-
if @target.options['cleanup']
|
141
|
-
_, stderr, exitcode = execute('rm', '-rf', dir, {})
|
142
|
-
if exitcode != 0
|
143
|
-
@logger.warn("Failed to clean up tmpdir '#{dir}': #{stderr}")
|
144
|
-
end
|
145
|
-
else
|
146
|
-
@logger.warn("Skipping cleanup of tmpdir '#{dir}'")
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
113
|
+
# Executes a Docker CLI command. This is useful for running commands as
|
114
|
+
# part of this class without having to go through the `execute`
|
115
|
+
# function and manage pipes.
|
116
|
+
#
|
117
|
+
# @param subcommand [String] The docker subcommand to run
|
118
|
+
# e.g. 'inspect' for `docker inspect`
|
119
|
+
# @param arguments [Array] Arguments to pass to the docker command
|
120
|
+
# e.g. 'src' and 'dest' for `docker cp <src> <dest>
|
121
|
+
# @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
|
122
|
+
private def execute_local_command(subcommand, arguments = [])
|
123
|
+
# Set the DOCKER_HOST if we are using a non-default service-url
|
124
|
+
env_hash = @docker_host.nil? ? {} : { 'DOCKER_HOST' => @docker_host }
|
125
|
+
docker_command = [subcommand].concat(arguments)
|
150
126
|
|
151
|
-
|
152
|
-
filename ||= File.basename(file)
|
153
|
-
remote_path = File.join(dir.to_s, filename)
|
154
|
-
write_remote_file(file, remote_path)
|
155
|
-
make_executable(remote_path)
|
156
|
-
remote_path
|
127
|
+
Open3.capture3(env_hash, 'docker', *docker_command, { binmode: true })
|
157
128
|
end
|
158
129
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
130
|
+
# Executes a Docker CLI command and parses the output in JSON format
|
131
|
+
#
|
132
|
+
# @param subcommand [String] The docker subcommand to run
|
133
|
+
# e.g. 'inspect' for `docker inspect`
|
134
|
+
# @param arguments [Array] Arguments to pass to the docker command
|
135
|
+
# e.g. 'src' and 'dest' for `docker cp <src> <dest>
|
136
|
+
# @return [Object] Ruby object representation of the JSON string
|
137
|
+
private def execute_local_json_command(subcommand, arguments = [])
|
138
|
+
command_options = ['--format', '{{json .}}'].concat(arguments)
|
139
|
+
stdout, _stderr, _status = execute_local_command(subcommand, command_options)
|
140
|
+
extract_json(stdout)
|
165
141
|
end
|
166
142
|
|
167
|
-
private
|
168
|
-
|
169
143
|
# Converts the JSON encoded STDOUT string from the docker cli into ruby objects
|
170
144
|
#
|
171
145
|
# @param stdout_string [String] The string to convert
|
172
146
|
# @return [Object] Ruby object representation of the JSON string
|
173
|
-
def extract_json(
|
147
|
+
private def extract_json(stdout)
|
174
148
|
# The output from the docker format command is a JSON string per line.
|
175
149
|
# We can't do a direct convert but this helper method will convert it into
|
176
150
|
# an array of Objects
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
end
|
181
|
-
|
182
|
-
# rubocop:disable Layout/LineLength
|
183
|
-
# Executes a Docker CLI command
|
184
|
-
#
|
185
|
-
# @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
|
186
|
-
# @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
|
187
|
-
# @param redir_stdin [IO] IO object which will be use to as STDIN in the docker command. Default is nil, which does not perform redirection
|
188
|
-
# @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
|
189
|
-
# rubocop:enable Layout/LineLength
|
190
|
-
def execute_local_docker_command(subcommand, command_options = [], redir_stdin = nil)
|
191
|
-
env_hash = {}
|
192
|
-
# Set the DOCKER_HOST if we are using a non-default service-url
|
193
|
-
env_hash['DOCKER_HOST'] = @docker_host unless @docker_host.nil?
|
194
|
-
|
195
|
-
command_options = [] if command_options.nil?
|
196
|
-
docker_command = [subcommand].concat(command_options)
|
197
|
-
|
198
|
-
# Always use binary mode for any text data
|
199
|
-
capture_options = { binmode: true }
|
200
|
-
capture_options[:stdin_data] = redir_stdin unless redir_stdin.nil?
|
201
|
-
stdout_str, stderr_str, status = Open3.capture3(env_hash, 'docker', *docker_command, capture_options)
|
202
|
-
[stdout_str, stderr_str, status]
|
203
|
-
end
|
204
|
-
|
205
|
-
# Executes a Docker CLI command and parses the output in JSON format
|
206
|
-
#
|
207
|
-
# @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
|
208
|
-
# @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
|
209
|
-
# @return [Object] Ruby object representation of the JSON string
|
210
|
-
def execute_local_docker_json_command(subcommand, command_options = [])
|
211
|
-
command_options = [] if command_options.nil?
|
212
|
-
command_options = ['--format', '{{json .}}'].concat(command_options)
|
213
|
-
stdout_str, _stderr_str, _status = execute_local_docker_command(subcommand, command_options)
|
214
|
-
extract_json(stdout_str)
|
215
|
-
end
|
216
|
-
|
217
|
-
# The full ID of the target container
|
218
|
-
#
|
219
|
-
# @return [String] The full ID of the target container
|
220
|
-
def container_id
|
221
|
-
@container_info["Id"]
|
222
|
-
end
|
223
|
-
|
224
|
-
# The temp path inside the target container
|
225
|
-
#
|
226
|
-
# @return [String] The absolute path to the temp directory
|
227
|
-
def container_tmpdir
|
228
|
-
'/tmp'
|
151
|
+
stdout.split("\n")
|
152
|
+
.reject { |str| str.strip.empty? }
|
153
|
+
.map { |str| JSON.parse(str) }
|
229
154
|
end
|
230
155
|
end
|
231
156
|
end
|