cem_acpt 0.8.8 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/spec.yml +0 -3
- data/Gemfile.lock +9 -1
- data/README.md +95 -13
- data/cem_acpt.gemspec +2 -1
- data/lib/cem_acpt/action_result.rb +8 -2
- data/lib/cem_acpt/actions.rb +153 -0
- data/lib/cem_acpt/bolt/cmd/base.rb +174 -0
- data/lib/cem_acpt/bolt/cmd/output.rb +315 -0
- data/lib/cem_acpt/bolt/cmd/task.rb +59 -0
- data/lib/cem_acpt/bolt/cmd.rb +22 -0
- data/lib/cem_acpt/bolt/errors.rb +49 -0
- data/lib/cem_acpt/bolt/helpers.rb +52 -0
- data/lib/cem_acpt/bolt/inventory.rb +62 -0
- data/lib/cem_acpt/bolt/project.rb +38 -0
- data/lib/cem_acpt/bolt/summary_results.rb +96 -0
- data/lib/cem_acpt/bolt/tasks.rb +181 -0
- data/lib/cem_acpt/bolt/tests.rb +415 -0
- data/lib/cem_acpt/bolt/yaml_file.rb +74 -0
- data/lib/cem_acpt/bolt.rb +142 -0
- data/lib/cem_acpt/cli.rb +6 -0
- data/lib/cem_acpt/config/base.rb +4 -0
- data/lib/cem_acpt/config/cem_acpt.rb +7 -1
- data/lib/cem_acpt/core_ext.rb +25 -0
- data/lib/cem_acpt/goss/api/action_response.rb +4 -0
- data/lib/cem_acpt/goss/api.rb +23 -25
- data/lib/cem_acpt/logging/formatter.rb +3 -3
- data/lib/cem_acpt/logging.rb +17 -1
- data/lib/cem_acpt/test_data.rb +2 -0
- data/lib/cem_acpt/test_runner/log_formatter/base.rb +73 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_error_formatter.rb +65 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_output_formatter.rb +54 -0
- data/lib/cem_acpt/test_runner/log_formatter/bolt_summary_results_formatter.rb +64 -0
- data/lib/cem_acpt/test_runner/log_formatter/goss_action_response.rb +17 -30
- data/lib/cem_acpt/test_runner/log_formatter/goss_error_formatter.rb +31 -0
- data/lib/cem_acpt/test_runner/log_formatter/standard_error_formatter.rb +35 -0
- data/lib/cem_acpt/test_runner/log_formatter.rb +17 -5
- data/lib/cem_acpt/test_runner/test_results.rb +150 -0
- data/lib/cem_acpt/test_runner.rb +153 -53
- data/lib/cem_acpt/utils/files.rb +189 -0
- data/lib/cem_acpt/utils/finalizer_queue.rb +73 -0
- data/lib/cem_acpt/utils/shell.rb +13 -4
- data/lib/cem_acpt/version.rb +1 -1
- data/sample_config.yaml +13 -0
- metadata +41 -5
- data/lib/cem_acpt/test_runner/log_formatter/error_formatter.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcea95d44401d3a90c8724ac2dbb85753eda2f38564ce84192f5580e1d528eaf
|
4
|
+
data.tar.gz: 6b2b8f267e951c578b35d49a11180bc8640da1695352097dd6652fc3e401aea9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbe2360892776beb52c7d83bda5813283fc8b26c43148a45a60c5d8be03d45104976d4a91e48dc4a0207092b65712fd87224df40844cc4000628869fd24f5824
|
7
|
+
data.tar.gz: a6cfb5bc229561cdd0905d15c969de745bd14e5a767a60099543ca579b72659f6abc09cfa4182a44634552e23ace099c38095cc9bdfd06d471a436adebbd76a3
|
data/.github/workflows/spec.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cem_acpt (0.
|
4
|
+
cem_acpt (0.9.0)
|
5
5
|
async-http (>= 0.60, < 0.70)
|
6
6
|
bcrypt_pbkdf (>= 1.0, < 2.0)
|
7
7
|
deep_merge (>= 1.2, < 2.0)
|
@@ -36,6 +36,7 @@ GEM
|
|
36
36
|
fiber-local
|
37
37
|
deep_merge (1.2.2)
|
38
38
|
diff-lcs (1.5.0)
|
39
|
+
docile (1.4.0)
|
39
40
|
ed25519 (1.3.0)
|
40
41
|
erubi (1.12.0)
|
41
42
|
ffi (1.15.5)
|
@@ -115,6 +116,12 @@ GEM
|
|
115
116
|
rubocop-factory_bot (~> 2.22)
|
116
117
|
ruby-progressbar (1.13.0)
|
117
118
|
rubyntlm (0.6.3)
|
119
|
+
simplecov (0.21.2)
|
120
|
+
docile (~> 1.1)
|
121
|
+
simplecov-html (~> 0.11)
|
122
|
+
simplecov_json_formatter (~> 0.1)
|
123
|
+
simplecov-html (0.12.3)
|
124
|
+
simplecov_json_formatter (0.1.4)
|
118
125
|
timers (4.3.5)
|
119
126
|
traces (0.9.1)
|
120
127
|
unicode-display_width (2.4.2)
|
@@ -141,6 +148,7 @@ DEPENDENCIES
|
|
141
148
|
rubocop
|
142
149
|
rubocop-performance
|
143
150
|
rubocop-rspec
|
151
|
+
simplecov
|
144
152
|
|
145
153
|
BUNDLED WITH
|
146
154
|
2.4.19
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ CemAcpt uses [Terraform](#terraform) for provisioning test nodes, and [Goss](#go
|
|
10
10
|
gem install cem_acpt
|
11
11
|
```
|
12
12
|
|
13
|
-
`cem_acpt` was developed using Ruby 3.2.1, but other Ruby versions
|
13
|
+
`cem_acpt` was developed using Ruby 3.2.1, but other Ruby versions `>= 3.0.0` should work.
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
@@ -101,6 +101,88 @@ To aide in development, you can enable tracing on CemAcpt's Ruby code execution
|
|
101
101
|
|
102
102
|
CemAcpt uses [Terraform](https://www.terraform.io/) for managing the lifecycle of test nodes. Users don't interact with Terraform directly, but you will need it installed to use CemAcpt.
|
103
103
|
|
104
|
+
## Bolt task testing
|
105
|
+
|
106
|
+
CemAcpt can execute Bolt tasks against the test nodes and perform basic validation against their status and outputs.
|
107
|
+
|
108
|
+
### Configuring Bolt tests
|
109
|
+
|
110
|
+
Bolt tests expose the following configuration options that can be set via a config file:
|
111
|
+
|
112
|
+
* `bolt.inventory_path` - A relative or absolute path to an existing inventory file, or the where the inventory file will be created
|
113
|
+
* `bolt.project.name` - The name of the Bolt project to be created and used during the Bolt tests
|
114
|
+
* `bolt.project.analytics` - Whether or not to enable Bolt analytics. Should normally be `false`.
|
115
|
+
* `bolt.project.path` - A relative or absolute path to an existing project file, or where the project file will be created
|
116
|
+
* `bolt.tests.only` - An array of acceptance tests to only run Bolt tests for. When acceptance test names are specified here, Bolt tasks will only be ran against the nodes created for those acceptance tests.
|
117
|
+
* `bolt.tests.ignore` - An array of acceptance tests to not run Bolt tests for. When acceptance test names are specified here, Bolt tasks will not be ran against the nodes created for those acceptance tests.
|
118
|
+
* `bolt.tasks.only` - An array of Bolt tasks to only run against nodes. Bolt tasks not listed here will not be ran.
|
119
|
+
* `bolt.tasks.ignore` - An array of Bolt tasks to not run against nodes. Bolt tasks specified here will not be ran at all.
|
120
|
+
* `bolt.tasks.module_pattern` - If specified, will only run Bolt tasks whose module prefix matches the specified pattern will be ran. The module prefix is the first part of the full task name before the first `::`. This option is interpreted as a RegEx pattern. For example, if `bolt.tasks.module_prefix` is set to `^our_module`, and our we have two tasks, `our_module::the_task` and `other_module::another_task`, only `our_module::task` will be ran.
|
121
|
+
* `bolt.tasks.name_filter` - Similar to `module_pattern`, but works on the portion of the task name after the module prefix and is exclusionary. For example, if `bolt.tasks.name_filter` is set to `^another_`, and our we have two tasks, `our_module::the_task` and `other_module::another_task`, only `our_module::the_task` will be ran.
|
122
|
+
|
123
|
+
### Creating Bolt tests
|
124
|
+
|
125
|
+
Bolt tests are not mandatory as all Bolt tasks that run will automatically be validated based on if they run successfully or not. However, sometimes we may want to test certain aspects of a Bolt task's output or pass a Bolt task parameters. To do this, we need to create a Bolt test file.
|
126
|
+
|
127
|
+
Bolt test files are YAML files named `bolt.yaml` that live inside the individual acceptance test directories. For each acceptance test, only one `bolt.yaml` file should exist. The `bolt.yaml` file consists of one or more YAML hashes that take the following form:
|
128
|
+
|
129
|
+
```yaml
|
130
|
+
'module_name::task_name':
|
131
|
+
params: # Optional, if you want to specify params to pass to the task at runtime
|
132
|
+
param_name: 'param_string_value'
|
133
|
+
status: <success | failure | skipped> # Optional, this is set to 'success' by default
|
134
|
+
other_bolt_json_output_keys: # Optional
|
135
|
+
match: '^a string RegEx pattern$' # Optional
|
136
|
+
not_match: '^a string RegEx pattern$' # Optional
|
137
|
+
```
|
138
|
+
|
139
|
+
All Bolt tasks are ran with the `--format json` flag, which returns a JSON document of their output. The keys of this document can be validated to either a specified pattern, not match a specified pattern, or both. You can also specify a string value for a key, and then the key's value with be compared for simple string equality with the given value.
|
140
|
+
|
141
|
+
Example:
|
142
|
+
|
143
|
+
In this example, we will go over how a Bolt task's output would be evaluated by a Bolt test hash. This example assumes one Bolt task named `cem_linux::audit_sssd_certmap` that takes no parameters was ran successfully against two nodes.
|
144
|
+
|
145
|
+
Bolt task JSON output:
|
146
|
+
|
147
|
+
```json
|
148
|
+
{
|
149
|
+
"items": [
|
150
|
+
{
|
151
|
+
"target":"35.212.146.14",
|
152
|
+
"action":"task",
|
153
|
+
"object":"cem_linux::audit_sssd_certmap",
|
154
|
+
"status":"success",
|
155
|
+
"value":{
|
156
|
+
"sssd_certmap_exists":false
|
157
|
+
}
|
158
|
+
},
|
159
|
+
{
|
160
|
+
"target":"35.212.197.53",
|
161
|
+
"action":"task",
|
162
|
+
"object":"cem_linux::audit_sssd_certmap",
|
163
|
+
"status":"success",
|
164
|
+
"value":{
|
165
|
+
"sssd_certmap_exists":false
|
166
|
+
}
|
167
|
+
}
|
168
|
+
],
|
169
|
+
"target_count": 2,
|
170
|
+
"elapsed_time": 7
|
171
|
+
}
|
172
|
+
```
|
173
|
+
|
174
|
+
Bolt test hash in `bolt.yaml`:
|
175
|
+
|
176
|
+
```yaml
|
177
|
+
'cem_linux::audit_sssd_certmap':
|
178
|
+
status: 'success'
|
179
|
+
value:
|
180
|
+
match: 'false'
|
181
|
+
```
|
182
|
+
|
183
|
+
This should result in the following log message after a run:
|
184
|
+
`INFO: CemAcpt: SUMMARY: Bolt tests: status: passed, tests total: 1, tests succeeded: 1, tests failed: 0`
|
185
|
+
|
104
186
|
## Platforms
|
105
187
|
|
106
188
|
Platforms are the underlying infrastructure that nodes are provisioned on. Currently, only GCP is supported. Each platform has two parts to it: the [platform](#platform) and the [node data](#node-data).
|
@@ -202,18 +284,18 @@ Much like `name_pattern_vars`, specifying the `image_name_builder` top-level key
|
|
202
284
|
|
203
285
|
## The acceptance test lifecycle
|
204
286
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
287
|
+
* Load and merge the config
|
288
|
+
* Create the local test directory under `$HOME/.cem_acpt`
|
289
|
+
* Build the Puppet module. Uses current dir if no `module_dir` is specified in the config
|
290
|
+
* Copy all relevant files into the local test directory
|
291
|
+
* This includes the Terraform files provided by CemAcpt, as well as the files under the specified acceptance test directory, and the built Puppet module
|
292
|
+
* Provision test nodes using Terraform
|
293
|
+
* After the node is created, the contents of the local test directory are copied to the node
|
294
|
+
* Additionally, the Puppet module is installed on the node and Puppet is ran once to apply `manifest.pp`
|
295
|
+
* After, the Goss server endpoints are started and exposed on the node
|
296
|
+
* Once node is provisioned, make HTTP get requests to the Goss server endpoints to run the tests
|
297
|
+
* Destroy the test nodes
|
298
|
+
* Report the results of the tests
|
217
299
|
|
218
300
|
## Generating acceptance test node images
|
219
301
|
|
data/cem_acpt.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = 'Litmus-like library focusing on CEM Acceptance Tests'
|
13
13
|
spec.homepage = 'https://github.com/puppetlabs/cem_acpt'
|
14
14
|
spec.license = 'proprietary'
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new('>=
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
|
16
16
|
|
17
17
|
spec.metadata['homepage_uri'] = spec.homepage
|
18
18
|
spec.metadata['source_code_uri'] = 'https://github.com/puppetlabs/cem_acpt'
|
@@ -37,4 +37,5 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.add_development_dependency 'rubocop'
|
38
38
|
spec.add_development_dependency 'rubocop-performance'
|
39
39
|
spec.add_development_dependency 'rubocop-rspec'
|
40
|
+
spec.add_development_dependency 'simplecov'
|
40
41
|
end
|
@@ -4,16 +4,22 @@ module CemAcpt
|
|
4
4
|
# Wrapper class for the result of an action. Provides a common interface for
|
5
5
|
# getting reportable data from the result.
|
6
6
|
class ActionResult
|
7
|
+
attr_reader :result
|
8
|
+
|
7
9
|
def initialize(result)
|
8
10
|
@result = if result.instance_of?(CemAcpt::Goss::Api::ActionResponse)
|
9
11
|
result
|
10
|
-
|
12
|
+
elsif result.instance_of?(CemAcpt::Bolt::Cmd::Output)
|
13
|
+
result
|
14
|
+
elsif result.is_a?(StandardError)
|
11
15
|
ErrorActionResult.new(result)
|
16
|
+
else
|
17
|
+
raise ArgumentError, "result must be a CemAcpt::Goss::Api::ActionResponse, CemAcpt::Bolt::Cmd::Output, or StandardError, got #{result.class}"
|
12
18
|
end
|
13
19
|
end
|
14
20
|
|
15
21
|
def error?
|
16
|
-
@result.is_a?(ErrorActionResult)
|
22
|
+
@result.is_a?(ErrorActionResult) || (@result.respond_to?(:error?) && @result.error?)
|
17
23
|
end
|
18
24
|
|
19
25
|
def method_missing(method_name, *args, **kwargs, &block)
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'async'
|
4
|
+
require 'async/barrier'
|
5
|
+
require 'async/http/internet'
|
6
|
+
|
7
|
+
module CemAcpt
|
8
|
+
# Provides a way to register actions to be executed if the registered
|
9
|
+
# actions are included in the list of actions to be executed.
|
10
|
+
module Actions
|
11
|
+
# Represents an action to be executed.
|
12
|
+
class Action
|
13
|
+
attr_reader :name, :order
|
14
|
+
|
15
|
+
def initialize(name, order = 0, &block)
|
16
|
+
@name = name.to_sym
|
17
|
+
@order = order
|
18
|
+
@block = block
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(context)
|
22
|
+
@block&.call(context)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Represents a group of actions.
|
27
|
+
class ActionGroup
|
28
|
+
attr_reader :name, :actions, :async, :order
|
29
|
+
|
30
|
+
def initialize(name = :main, **opts)
|
31
|
+
@name = name.to_sym
|
32
|
+
@actions = []
|
33
|
+
@async = opts[:async] || false
|
34
|
+
@order = opts[:order] || 0
|
35
|
+
end
|
36
|
+
|
37
|
+
# Registers a block to be executed if the registered actions are
|
38
|
+
# included in the list of actions to be executed.
|
39
|
+
# @param action [String, Symbol] the name of the action to be registered
|
40
|
+
# @param order [Integer] the order in which the action should be executed.
|
41
|
+
# Lower numbers are executed first, but this order is relative to the
|
42
|
+
# order of the associated action groups' execution order. Defaults to 0.
|
43
|
+
# @param block [Proc] the block to be executed
|
44
|
+
def register_action(name, order: 0, &block)
|
45
|
+
new_action = Action.new(name, order, &block)
|
46
|
+
@actions << new_action
|
47
|
+
sort!
|
48
|
+
self # return self to allow chaining
|
49
|
+
end
|
50
|
+
|
51
|
+
def filter_actions
|
52
|
+
filtered = @actions.dup
|
53
|
+
only = CemAcpt::Actions.config.only
|
54
|
+
except = CemAcpt::Actions.config.except
|
55
|
+
filtered.select! { |action| only.include?(action.name) } unless only.empty?
|
56
|
+
filtered.reject! { |action| except.include?(action.name) } unless except.empty?
|
57
|
+
filtered.sort_by!(&:order)
|
58
|
+
filtered
|
59
|
+
end
|
60
|
+
|
61
|
+
def sort!
|
62
|
+
@actions.sort_by!(&:order)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Represents the configuration for the Actions module.
|
67
|
+
class ActionConfig
|
68
|
+
attr_reader :groups, :only, :except
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
@groups = {
|
72
|
+
main: ActionGroup.new,
|
73
|
+
}
|
74
|
+
@only = []
|
75
|
+
@except = []
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the action group with the specified name.
|
79
|
+
# @param key [String, Symbol] the name of the action group
|
80
|
+
def [](key)
|
81
|
+
groups[key.to_sym]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Registers an action group. Actions groups are logical groups of
|
85
|
+
# actions that can be executed in parallel.
|
86
|
+
# @param name [String, Symbol] the name of the action group
|
87
|
+
# @param async [Boolean] whether the actions in the group should be
|
88
|
+
# executed in parallel. Defaults to false.
|
89
|
+
# @param order [Integer] the order in which the action group should
|
90
|
+
# be executed. Lower numbers are executed first.
|
91
|
+
def register_group(name, **opts)
|
92
|
+
return groups[name] if groups.key?(name)
|
93
|
+
|
94
|
+
groups[name] = ActionGroup.new(name, **opts)
|
95
|
+
groups[name]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns a list of the names of all registered actions.
|
99
|
+
# @return [Array<String>] the list of all registered action names
|
100
|
+
def action_names(include_all: false)
|
101
|
+
return groups.values.map { |a| a.actions }.flatten.map(&:name).uniq if include_all
|
102
|
+
|
103
|
+
groups.values.map { |a| a.filter_actions }.flatten.map(&:name).uniq
|
104
|
+
end
|
105
|
+
|
106
|
+
def only=(actions)
|
107
|
+
raise ArgumentError, 'only must be an array' unless actions.is_a?(Array)
|
108
|
+
|
109
|
+
@only = actions.map(&:to_sym)
|
110
|
+
end
|
111
|
+
|
112
|
+
def except=(actions)
|
113
|
+
raise ArgumentError, 'except must be an array' unless actions.is_a?(Array)
|
114
|
+
|
115
|
+
@except = actions.map(&:to_sym)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class << self
|
120
|
+
attr_reader :config
|
121
|
+
|
122
|
+
# Configures the Actions module.
|
123
|
+
# @param world_config [CemAcpt::Config::Base] the current config for the "world"
|
124
|
+
# @yield [CemAcpt::Actions::ActionConfig] the config object for the Actions module
|
125
|
+
def configure(world_config)
|
126
|
+
@config = ActionConfig.new
|
127
|
+
@config.only = world_config.get('actions.only') || []
|
128
|
+
@config.except = world_config.get('actions.except') || []
|
129
|
+
yield @config if block_given?
|
130
|
+
end
|
131
|
+
|
132
|
+
# Executes the actions in the current action groups.
|
133
|
+
# @param opts [Hash] the options to be passed to the actions
|
134
|
+
# @option opts [Hash] :context the context to be passed to the actions
|
135
|
+
# @return [Array] the results of the actions
|
136
|
+
def execute(**opts)
|
137
|
+
context = opts[:context] || {}
|
138
|
+
ordered_groups = config.groups.values.sort_by(&:order)
|
139
|
+
ordered_groups.each_with_object([]) do |group, results|
|
140
|
+
actions = group.filter_actions
|
141
|
+
next if actions.empty?
|
142
|
+
|
143
|
+
context[:group] = group.name
|
144
|
+
context[:actions] = actions.map(&:name)
|
145
|
+
actions.each do |action|
|
146
|
+
action_opts = opts[action.name.to_sym] || {}
|
147
|
+
results << action.call(context.merge(action_opts))
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require_relative 'output'
|
5
|
+
require_relative '../../logging'
|
6
|
+
require_relative '../../utils/shell'
|
7
|
+
|
8
|
+
module CemAcpt
|
9
|
+
module Bolt
|
10
|
+
module Cmd
|
11
|
+
# Base class for all Bolt command classes
|
12
|
+
class Base
|
13
|
+
include CemAcpt::Logging
|
14
|
+
|
15
|
+
# Class method that holds the names of all options for the command
|
16
|
+
def self.option_names
|
17
|
+
@option_names ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Class method that defines an option for the command
|
21
|
+
# @param name [String, Symbol] The name of the option
|
22
|
+
# @param flag [String] The flag to use for the option. Ex: '--modulepath'
|
23
|
+
# @param config_path [String] The path to the config value for the option. Ex: 'bolt.module_path'
|
24
|
+
# @param default [String] The default value for the option. Ex: '/home/user/.puppetlabs/bolt/modules'
|
25
|
+
# @param bool_flag [Boolean] Whether the option is a boolean flag or not. If this is true,
|
26
|
+
# only the flag will be used for the option. For example, if the flag is '--clear-cache'
|
27
|
+
# and bool_flag is set to true, and the value is 'true', then just '--clear-cache' will
|
28
|
+
# be added to the command options. This differs from the default behavior of adding
|
29
|
+
# '--clear-cache <value>' to the command options.
|
30
|
+
def self.option(name, flag, config_path: nil, default: nil, bool_flag: false)
|
31
|
+
name = name.to_sym
|
32
|
+
option_names << name
|
33
|
+
|
34
|
+
define_method(name) do
|
35
|
+
if bool_flag
|
36
|
+
find_val(name, flag: flag, config_path: config_path, default: default) ? flag : nil
|
37
|
+
else
|
38
|
+
val = find_val(name, flag: flag, config_path: config_path, default: default)
|
39
|
+
val ? "#{flag} #{val}" : nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Denotes that the command supports parameters. Calling this method will define a
|
45
|
+
# supports_params? method on the command class that returns true.
|
46
|
+
def self.supports_params
|
47
|
+
define_method(:supports_params?) { true }
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :cmd_env, :cmd_params
|
51
|
+
|
52
|
+
def initialize
|
53
|
+
@cmd_env = { 'BOLT_GEM' => 'TRUE' }
|
54
|
+
@cmd_params = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def inspect
|
58
|
+
"#{self.class}:\"#{cmd}\""
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
cmd
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the command to run as a string
|
66
|
+
# @return [String] The command to run as a string
|
67
|
+
def cmd
|
68
|
+
raise NotImplementedError, 'cmd method must be implemented in subclass'
|
69
|
+
end
|
70
|
+
|
71
|
+
# Adds an environment variable to the command
|
72
|
+
# @param name [String] The name of the environment variable
|
73
|
+
# @param value [String] The value of the environment variable
|
74
|
+
def add_cmd_env(name, value)
|
75
|
+
@cmd_env[name] = value
|
76
|
+
end
|
77
|
+
|
78
|
+
# Adds a parameter to the command. If passing a complex value, such as a hash, use
|
79
|
+
# the JSON representation of the value.
|
80
|
+
# @param name [String] The name of the parameter
|
81
|
+
# @param value [String] The value of the parameter
|
82
|
+
def add_cmd_param(name, value)
|
83
|
+
@cmd_params[name] = value
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the family of the command. This is used to determine which command to run
|
87
|
+
# when multiple commands are available for the same action. For example, the 'task'
|
88
|
+
# command has multiple subcommands, such as 'task show' and 'task run'. The family of the
|
89
|
+
# 'task' command is 'task', and the family of the 'task show' and 'task run' subcommands is
|
90
|
+
# 'task'.
|
91
|
+
# @return [String] The family of the command
|
92
|
+
def command_family
|
93
|
+
raise NotImplementedError, 'command_family method must be implemented in subclass'
|
94
|
+
end
|
95
|
+
|
96
|
+
# Runs the command with the given options and environment variables
|
97
|
+
# @param strict [Boolean] Whether to raise an error if the command returns output that
|
98
|
+
# does not implement items. Used for commands where the raw JSON output does not
|
99
|
+
# include an 'items' key. Defaults to true.
|
100
|
+
# @param params [Hash] Parameters to pass to the Bolt command. Only used for some command types.
|
101
|
+
# @return [CemAcpt::Bolt::Cmd::Output] The output of the command
|
102
|
+
def run(strict: true, **params)
|
103
|
+
unless params.empty?
|
104
|
+
logger.debug('CemAcpt::Bolt') { "Setting command parameters: #{params}" }
|
105
|
+
params.each { |k, v| add_cmd_param(k, v) }
|
106
|
+
end
|
107
|
+
logger.debug('CemAcpt::Bolt') { "Running Bolt command: #{cmd}" }
|
108
|
+
logger.debug('CemAcpt::Bolt') { "Bolt command environment: #{cmd_env}" }
|
109
|
+
begin
|
110
|
+
out = CemAcpt::Utils::Shell.run_cmd(cmd, cmd_env, output: nil, raise_on_fail: false)
|
111
|
+
logger.debug('CemAcpt::Bolt') { "Bolt command raw output:\n#{out}" }
|
112
|
+
CemAcpt::Bolt::Cmd::Output.new(out, strict: strict, **(@item_defaults || {}))
|
113
|
+
rescue StandardError => e
|
114
|
+
logger.debug('CemAcpt::Bolt') { "Bolt command error:\n#{e}" }
|
115
|
+
CemAcpt::Bolt::Cmd::Output.new(e, **(@item_defaults || {}))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the options for the command as a string
|
120
|
+
# @return [String] The options for the command as a string
|
121
|
+
def options
|
122
|
+
opts = ['--format json']
|
123
|
+
recursive_option_names.each do |opt|
|
124
|
+
val = send(opt)
|
125
|
+
opts << val if val
|
126
|
+
end
|
127
|
+
if respond_to?(:supports_params?) && supports_params? && !cmd_params.empty?
|
128
|
+
opts << "--params '#{JSON.generate(cmd_params)}'"
|
129
|
+
end
|
130
|
+
join_array(opts)
|
131
|
+
end
|
132
|
+
|
133
|
+
def bolt_bin
|
134
|
+
@bolt_bin ||= CemAcpt::Utils::Shell.which('bolt', raise_if_not_found: true)
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
attr_reader :config
|
140
|
+
|
141
|
+
def join_array(ary)
|
142
|
+
ary.compact.join(' ')
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_val(name, flag: nil, config_path: nil, default: nil)
|
146
|
+
ivar_safe_get("@#{name}") ||
|
147
|
+
ivar_safe_get("@#{flag.to_s.gsub(%r{[-]+}, '')}") ||
|
148
|
+
config&.get(config_path.to_s) ||
|
149
|
+
config&.get("bolt.tasks.cmd.defaults.#{flag.to_s.gsub(%r{[-]+}, '')}") ||
|
150
|
+
default
|
151
|
+
end
|
152
|
+
|
153
|
+
def ivar_safe_get(name)
|
154
|
+
name = "@#{name}" unless name.to_s.start_with?('@')
|
155
|
+
instance_variable_get(name)
|
156
|
+
rescue StandardError
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Collects options defined in parent classes
|
161
|
+
def recursive_option_names(klass = self.class)
|
162
|
+
return [] if klass == Object
|
163
|
+
onames = klass.option_names.dup
|
164
|
+
|
165
|
+
if klass.superclass.respond_to?(:option_names)
|
166
|
+
onames.concat(recursive_option_names(klass.superclass))
|
167
|
+
else
|
168
|
+
onames
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|