moose-inventory 2.0 → 2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +2 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +21 -0
- data/BACKLOG.md +630 -8
- data/Gemfile +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +315 -39
- data/Rakefile +2 -0
- data/bin/moose-inventory +2 -1
- data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
- data/docs/compatibility/cli-output-compatibility.md +76 -0
- data/docs/governance/approval-register.md +37 -0
- data/docs/maintenance/database-backup-restore-guidance.md +162 -0
- data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
- data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
- data/docs/product/product-brief.md +161 -0
- data/docs/product/requirements-baseline.md +477 -0
- data/docs/qa/qa-documentation-and-release-gates.md +283 -0
- data/docs/release/package-provenance-hardening.md +126 -0
- data/docs/release/publishing.md +11 -3
- data/docs/release/release-environment-protection.md +70 -0
- data/docs/release/release-readiness.md +23 -4
- data/docs/security/accepted-risk-register.md +84 -0
- data/docs/security/security-privacy-process.md +287 -0
- data/docs/security-audit-2026-05-26-rerun.md +2 -2
- data/docs/ux/cli-workflow-notes.md +287 -0
- data/examples/ansible/ansible.cfg +3 -0
- data/examples/ansible/inventory/moose_inventory.yml +5 -0
- data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
- data/examples/ci/README.md +16 -0
- data/examples/ci/github-actions/inventory-review.yml +38 -0
- data/examples/ci/inventory/example-snapshot.yml +19 -0
- data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
- data/lib/moose_inventory/cli/application.rb +133 -5
- data/lib/moose_inventory/cli/association_rendering.rb +74 -0
- data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
- data/lib/moose_inventory/cli/audit.rb +62 -0
- data/lib/moose_inventory/cli/audit_recording.rb +40 -0
- data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
- data/lib/moose_inventory/cli/console.rb +135 -0
- data/lib/moose_inventory/cli/db.rb +64 -0
- data/lib/moose_inventory/cli/factory.rb +28 -0
- data/lib/moose_inventory/cli/formatter.rb +8 -12
- data/lib/moose_inventory/cli/group.rb +5 -2
- data/lib/moose_inventory/cli/group_add.rb +11 -9
- data/lib/moose_inventory/cli/group_addchild.rb +23 -65
- data/lib/moose_inventory/cli/group_addhost.rb +16 -67
- data/lib/moose_inventory/cli/group_addvar.rb +27 -47
- data/lib/moose_inventory/cli/group_get.rb +8 -42
- data/lib/moose_inventory/cli/group_list.rb +7 -40
- data/lib/moose_inventory/cli/group_listvars.rb +9 -55
- data/lib/moose_inventory/cli/group_rm.rb +12 -10
- data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
- data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
- data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
- data/lib/moose_inventory/cli/group_tags.rb +33 -0
- data/lib/moose_inventory/cli/helpers.rb +68 -1
- data/lib/moose_inventory/cli/host.rb +6 -3
- data/lib/moose_inventory/cli/host_add.rb +69 -29
- data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
- data/lib/moose_inventory/cli/host_addvar.rb +28 -52
- data/lib/moose_inventory/cli/host_get.rb +9 -37
- data/lib/moose_inventory/cli/host_list.rb +24 -21
- data/lib/moose_inventory/cli/host_listvars.rb +9 -62
- data/lib/moose_inventory/cli/host_rm.rb +60 -42
- data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
- data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
- data/lib/moose_inventory/cli/host_tags.rb +33 -0
- data/lib/moose_inventory/cli/listvars_support.rb +55 -0
- data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
- data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
- data/lib/moose_inventory/cli/tag_support.rb +97 -0
- data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
- data/lib/moose_inventory/config/config.rb +185 -108
- data/lib/moose_inventory/db/db.rb +170 -195
- data/lib/moose_inventory/db/exceptions.rb +6 -3
- data/lib/moose_inventory/db/models.rb +16 -0
- data/lib/moose_inventory/db/schema_migrations.rb +248 -0
- data/lib/moose_inventory/inventory_context.rb +68 -2
- data/lib/moose_inventory/operations/add_associations.rb +20 -16
- data/lib/moose_inventory/operations/add_groups.rb +21 -13
- data/lib/moose_inventory/operations/add_hosts.rb +30 -17
- data/lib/moose_inventory/operations/add_variables.rb +77 -0
- data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
- data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
- data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
- data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
- data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
- data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
- data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
- data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
- data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
- data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
- data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
- data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
- data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
- data/lib/moose_inventory/operations/query_inventory.rb +47 -0
- data/lib/moose_inventory/operations/remove_associations.rb +30 -18
- data/lib/moose_inventory/operations/remove_groups.rb +12 -12
- data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
- data/lib/moose_inventory/operations/remove_variables.rb +67 -0
- data/lib/moose_inventory/runtime_options.rb +31 -0
- data/lib/moose_inventory/version.rb +3 -1
- data/lib/moose_inventory.rb +10 -7
- data/moose-inventory.gemspec +19 -35
- data/scripts/check.sh +1 -0
- data/scripts/ci/check_generated_artifacts.sh +41 -0
- data/scripts/ci/check_permissions.sh +2 -0
- data/scripts/ci/check_rubocop.sh +30 -25
- data/scripts/files.rb +5 -4
- data/spec/examples/ci_examples_spec.rb +37 -0
- data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
- data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
- data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
- data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
- data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
- data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
- data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
- data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
- data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
- data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
- data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
- data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
- data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
- data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
- data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
- data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
- data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
- data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
- data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
- data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
- data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
- data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
- data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
- data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
- data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
- data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
- data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
- data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
- data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
- data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
- data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
- data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
- data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
- data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
- data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
- data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
- data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
- data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
- data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
- data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
- data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
- data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
- data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
- data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
- data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
- data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
- data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
- data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
- data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
- data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
- data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
- data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
- data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
- data/spec/shared/shared_config_setup.rb +4 -3
- data/spec/spec_helper.rb +50 -40
- data/spec/support/cli_harness.rb +33 -0
- metadata +80 -41
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'Ansible inventory plugin examples' do
|
|
7
|
+
it 'ships a plugin, inventory source, and ansible.cfg example' do
|
|
8
|
+
expect(File).to exist('examples/ansible/inventory_plugins/moose_inventory.py')
|
|
9
|
+
expect(File).to exist('examples/ansible/inventory/moose_inventory.yml')
|
|
10
|
+
expect(File).to exist('examples/ansible/ansible.cfg')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'uses the moose_inventory plugin in the example inventory source' do
|
|
14
|
+
config = YAML.safe_load_file('examples/ansible/inventory/moose_inventory.yml')
|
|
15
|
+
|
|
16
|
+
expect(config).to include(
|
|
17
|
+
'plugin' => 'moose_inventory',
|
|
18
|
+
'executable' => 'moose-inventory',
|
|
19
|
+
'config' => './example.conf',
|
|
20
|
+
'env' => 'dev'
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'keeps the plugin source syntax-valid' do
|
|
25
|
+
result = system('python3', '-m', 'py_compile', 'examples/ansible/inventory_plugins/moose_inventory.py')
|
|
26
|
+
|
|
27
|
+
expect(result).to eq(true)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
|
4
|
+
require 'spec_helper'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moose::Inventory::Cli::Application do
|
|
7
|
+
before(:all) do
|
|
8
|
+
setup_cli_harness(command_class: described_class)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
before(:each) do
|
|
12
|
+
reset_cli_harness
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'prints a human success report when no issues are found' do
|
|
16
|
+
web = @db.models[:group].create(name: 'web')
|
|
17
|
+
host = @db.models[:host].create(name: 'web01')
|
|
18
|
+
host.add_group(web)
|
|
19
|
+
|
|
20
|
+
actual = runner { @app.start(%w[doctor]) }
|
|
21
|
+
|
|
22
|
+
expected(actual, aborted: false, STDOUT: "Inventory doctor found no issues.\n", STDERR: '')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'prints a human report and exits nonzero when issues are found' do
|
|
26
|
+
@db.models[:group].create(name: 'ungrouped')
|
|
27
|
+
host = @db.models[:host].create(name: 'lonely')
|
|
28
|
+
host.add_group(@db.models[:group].find(name: 'ungrouped'))
|
|
29
|
+
|
|
30
|
+
actual = runner { @app.start(%w[doctor]) }
|
|
31
|
+
|
|
32
|
+
expect(actual[:aborted]).to eq(true)
|
|
33
|
+
expect(actual[:STDOUT]).to include('Inventory doctor found')
|
|
34
|
+
expect(actual[:STDOUT]).to include('host_only_in_ungrouped')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'prints a machine-readable report when requested' do
|
|
38
|
+
@db.models[:group].create(name: 'ungrouped')
|
|
39
|
+
host = @db.models[:host].create(name: 'lonely')
|
|
40
|
+
host.add_group(@db.models[:group].find(name: 'ungrouped'))
|
|
41
|
+
|
|
42
|
+
actual = runner { @app.start(%w[doctor --format json]) }
|
|
43
|
+
|
|
44
|
+
expect(actual[:aborted]).to eq(true)
|
|
45
|
+
report = JSON.parse(actual[:STDOUT])
|
|
46
|
+
expect(report['ok']).to eq(false)
|
|
47
|
+
expect(report['issues'].map { |issue| issue['id'] }).to include('host_only_in_ungrouped')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Metrics/BlockLength
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
|
4
|
+
require 'spec_helper'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moose::Inventory::Cli::Application do
|
|
8
|
+
before(:all) do
|
|
9
|
+
setup_cli_harness(command_class: described_class)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before(:each) do
|
|
13
|
+
reset_cli_harness
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'exports the current inventory snapshot as yaml' do
|
|
17
|
+
runner { @app.start(%w[host add web01]) }
|
|
18
|
+
runner { @app.start(%w[group add web]) }
|
|
19
|
+
runner { @app.start(%w[host addgroup web01 web]) }
|
|
20
|
+
|
|
21
|
+
actual = runner { @app.start(%w[export]) }
|
|
22
|
+
|
|
23
|
+
expect(actual[:unexpected]).to eq(false)
|
|
24
|
+
expect(actual[:aborted]).to eq(false)
|
|
25
|
+
expect(actual[:STDERR]).to eq('')
|
|
26
|
+
snapshot = YAML.safe_load(actual[:STDOUT])
|
|
27
|
+
expect(snapshot['version']).to eq(1)
|
|
28
|
+
expect(snapshot.dig('hosts', 'web01', 'groups')).to include('web')
|
|
29
|
+
expect(snapshot['groups']).to have_key('web')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'imports a validated inventory snapshot file' do
|
|
33
|
+
Dir.mktmpdir do |dir|
|
|
34
|
+
path = File.join(dir, 'inventory.yml')
|
|
35
|
+
File.write(
|
|
36
|
+
path,
|
|
37
|
+
{
|
|
38
|
+
'version' => 1,
|
|
39
|
+
'hosts' => { 'web01' => { 'groups' => ['web'], 'tags' => [], 'vars' => { 'env' => 'prod' } } },
|
|
40
|
+
'groups' => { 'web' => { 'children' => [], 'tags' => [], 'vars' => { 'role' => 'frontend' } } }
|
|
41
|
+
}.to_yaml
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
actual = runner { @app.start(['import', path]) }
|
|
45
|
+
|
|
46
|
+
expect(actual[:unexpected]).to eq(false)
|
|
47
|
+
expect(actual[:aborted]).to eq(false)
|
|
48
|
+
expect(actual[:STDERR]).to eq('')
|
|
49
|
+
expect(actual[:STDOUT]).to include("Imported inventory snapshot from #{path}.")
|
|
50
|
+
host = @db.models[:host].find(name: 'web01')
|
|
51
|
+
expect(host.groups_dataset[name: 'web']).not_to be_nil
|
|
52
|
+
expect(host.hostvars_dataset[name: 'env'][:value]).to eq('prod')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'previews an inventory snapshot import without writing' do
|
|
57
|
+
runner { @app.start(%w[group add web]) }
|
|
58
|
+
|
|
59
|
+
Dir.mktmpdir do |dir|
|
|
60
|
+
path = File.join(dir, 'inventory.yml')
|
|
61
|
+
File.write(
|
|
62
|
+
path,
|
|
63
|
+
{
|
|
64
|
+
'version' => 1,
|
|
65
|
+
'hosts' => { 'web01' => { 'groups' => ['web'], 'tags' => [], 'vars' => { 'env' => 'prod' } } },
|
|
66
|
+
'groups' => { 'web' => { 'children' => [], 'tags' => [], 'vars' => {} } }
|
|
67
|
+
}.to_yaml
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
actual = runner { @app.start(['import', path, '--preview', '--preview-format', 'json']) }
|
|
71
|
+
|
|
72
|
+
expect(actual[:unexpected]).to eq(false)
|
|
73
|
+
expect(actual[:aborted]).to eq(false)
|
|
74
|
+
expect(actual[:STDERR]).to eq('')
|
|
75
|
+
preview = JSON.parse(actual[:STDOUT])
|
|
76
|
+
expect(preview['schema_version']).to eq('snapshot-import-preview-v1')
|
|
77
|
+
expect(preview['changes_applied']).to eq(false)
|
|
78
|
+
expect(preview.dig('summary', 'hosts_created')).to eq(1)
|
|
79
|
+
expect(preview.dig('creates', 'hosts')).to eq(['web01'])
|
|
80
|
+
expect(@db.models[:host].find(name: 'web01')).to be_nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'aborts on invalid snapshot input without writing' do
|
|
85
|
+
Dir.mktmpdir do |dir|
|
|
86
|
+
path = File.join(dir, 'inventory.yml')
|
|
87
|
+
File.write(
|
|
88
|
+
path,
|
|
89
|
+
{ 'version' => 1, 'hosts' => { 'web01' => { 'groups' => ['missing'], 'vars' => {} } }, 'groups' => {} }.to_yaml
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
actual = runner { @app.start(['import', path]) }
|
|
93
|
+
|
|
94
|
+
expect(actual[:aborted]).to eq(true)
|
|
95
|
+
expect(actual[:STDERR]).to include("Invalid inventory snapshot: host 'web01' references unknown group 'missing'.")
|
|
96
|
+
expect(@db.models[:host].count).to eq(0)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
# rubocop:enable Metrics/BlockLength
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moose::Inventory::Cli::Application do
|
|
6
|
+
describe 'version' do
|
|
7
|
+
it 'prints the current Moose Inventory version' do
|
|
8
|
+
output = capture(:STDOUT) do
|
|
9
|
+
described_class.start(%w[version])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
expect(output).to eq("Version #{Moose::Inventory::VERSION}\n")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'subcommands' do
|
|
17
|
+
it 'registers the group subcommand' do
|
|
18
|
+
expect(described_class.subcommands).to include('group')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'registers the host subcommand' do
|
|
22
|
+
expect(described_class.subcommands).to include('host')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'spec_helper'
|
|
5
|
+
|
|
6
|
+
# rubocop:disable Metrics/BlockLength
|
|
7
|
+
RSpec.describe Moose::Inventory::Cli::Audit do
|
|
8
|
+
before(:all) do
|
|
9
|
+
setup_cli_harness(command_class: Moose::Inventory::Cli::Audit)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before(:each) do
|
|
13
|
+
reset_cli_harness
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'starts with an empty audit log' do
|
|
17
|
+
actual = runner { @app.start(%w[audit list]) }
|
|
18
|
+
|
|
19
|
+
expected(actual, aborted: false, STDOUT: "No audit events recorded.\n", STDERR: '')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'records successful mutating host commands' do
|
|
23
|
+
add = runner { @app.start(%w[host add app01]) }
|
|
24
|
+
expect(add[:unexpected]).to eq(false)
|
|
25
|
+
|
|
26
|
+
event = @db.models[:audit_event].last
|
|
27
|
+
expect(event.command).to eq('host add')
|
|
28
|
+
expect(event.action).to eq('add')
|
|
29
|
+
expect(event.entity_type).to eq('host')
|
|
30
|
+
expect(event.entity_name).to eq('app01')
|
|
31
|
+
expect(JSON.parse(event.details)).to include('events')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'does not record dry-run commands' do
|
|
35
|
+
actual = runner { @app.start(%w[host add --dry-run app01]) }
|
|
36
|
+
expect(actual[:unexpected]).to eq(false)
|
|
37
|
+
|
|
38
|
+
expect(@db.models[:audit_event].count).to eq(0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'lists audit events as machine-readable JSON' do
|
|
42
|
+
runner { @app.start(%w[group add web]) }
|
|
43
|
+
|
|
44
|
+
actual = runner { @app.start(%w[audit list --format json]) }
|
|
45
|
+
parsed = JSON.parse(actual[:STDOUT])
|
|
46
|
+
|
|
47
|
+
expect(actual[:unexpected]).to eq(false)
|
|
48
|
+
expect(parsed.first).to include(
|
|
49
|
+
'command' => 'group add',
|
|
50
|
+
'action' => 'add',
|
|
51
|
+
'entity_type' => 'group',
|
|
52
|
+
'entity_name' => 'web'
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
# rubocop:enable Metrics/BlockLength
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
|
|
3
|
-
RSpec.describe Moose::Inventory::Cli
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
RSpec.describe Moose::Inventory::Cli do
|
|
6
|
+
describe '.start' do
|
|
7
|
+
it 'wires config, db, and application explicitly' do
|
|
8
|
+
args = ['--format', 'yaml']
|
|
9
|
+
config = instance_double('Config')
|
|
10
|
+
db = instance_double('DB')
|
|
11
|
+
application = class_double('Application')
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
expect(result).to eq(true)
|
|
13
|
-
end
|
|
13
|
+
expect(config).to receive(:init).with(args).ordered
|
|
14
|
+
expect(db).to receive(:init).ordered
|
|
15
|
+
expect(config).to receive(:application_args).ordered.and_return(['version'])
|
|
16
|
+
expect(application).to receive(:start).with(['version']).ordered
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# actual = runner { @app.version }
|
|
18
|
-
#
|
|
19
|
-
# desired = {}
|
|
20
|
-
# desired[:STDERR] = "Version #{Moose::Inventory::VERSION}"
|
|
21
|
-
#
|
|
22
|
-
# expected(actual, desired)
|
|
23
|
-
# end
|
|
18
|
+
described_class.start(args, config: config, db: db, application: application)
|
|
19
|
+
end
|
|
24
20
|
end
|
|
25
21
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
# rubocop:disable Metrics/BlockLength
|
|
7
|
+
RSpec.describe Moose::Inventory::Cli::Console do
|
|
8
|
+
before(:all) do
|
|
9
|
+
setup_cli_harness(command_class: Moose::Inventory::Cli::Application)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before(:each) do
|
|
13
|
+
reset_cli_harness
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run_console(input)
|
|
17
|
+
original_stdin = $stdin
|
|
18
|
+
$stdin = StringIO.new(input)
|
|
19
|
+
runner { @app.start(%w[console]) }
|
|
20
|
+
ensure
|
|
21
|
+
$stdin = original_stdin
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'prints help and exits' do
|
|
25
|
+
actual = run_console("help\nquit\n")
|
|
26
|
+
|
|
27
|
+
expect(actual[:unexpected]).to eq(false)
|
|
28
|
+
expect(actual[:STDOUT]).to include('Moose Inventory console (read-only). Type help or quit.')
|
|
29
|
+
expect(actual[:STDOUT]).to include('- hosts')
|
|
30
|
+
expect(actual[:STDOUT]).to include('- group NAME')
|
|
31
|
+
expect(actual[:STDOUT]).to include('Goodbye.')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'browses hosts, groups, tags, and entity detail without mutating' do
|
|
35
|
+
runner { @app.start(%w[group add web]) }
|
|
36
|
+
runner { @app.start(%w[host add web01 --groups web]) }
|
|
37
|
+
runner { @app.start(%w[host addtag web01 prod]) }
|
|
38
|
+
before_audit_count = @db.models[:audit_event].count
|
|
39
|
+
|
|
40
|
+
actual = run_console("hosts\ngroups\nhost web01\ntags host web01\nquit\n")
|
|
41
|
+
|
|
42
|
+
expect(actual[:unexpected]).to eq(false)
|
|
43
|
+
expect(actual[:STDOUT]).to include('Hosts: web01')
|
|
44
|
+
expect(actual[:STDOUT]).to include('Groups: web')
|
|
45
|
+
expect(actual[:STDOUT]).to include('Host: web01')
|
|
46
|
+
expect(actual[:STDOUT]).to include('Tags: prod')
|
|
47
|
+
expect(@db.models[:audit_event].count).to eq(before_audit_count)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'reports unknown and missing entities safely' do
|
|
51
|
+
actual = run_console("nonsense\nhost missing\nquit\n")
|
|
52
|
+
|
|
53
|
+
expect(actual[:unexpected]).to eq(false)
|
|
54
|
+
expect(actual[:STDOUT]).to include('Unknown command: nonsense')
|
|
55
|
+
expect(actual[:STDOUT]).to include("Host 'missing' not found.")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'supports quoted read-only entity names' do
|
|
59
|
+
group = @db.models[:group].create(name: 'prod group')
|
|
60
|
+
host = @db.models[:host].create(name: 'web 01')
|
|
61
|
+
tag = @db.models[:tag].create(name: 'critical host')
|
|
62
|
+
host.add_group(group)
|
|
63
|
+
host.add_tag(tag)
|
|
64
|
+
before_audit_count = @db.models[:audit_event].count
|
|
65
|
+
|
|
66
|
+
actual = run_console("host \"web 01\"\ntags host \"web 01\"\ngroup 'prod group'\nquit\n")
|
|
67
|
+
|
|
68
|
+
expect(actual[:unexpected]).to eq(false)
|
|
69
|
+
expect(actual[:STDOUT]).to include('Host: web 01')
|
|
70
|
+
expect(actual[:STDOUT]).to include('Groups: prod group')
|
|
71
|
+
expect(actual[:STDOUT]).to include('critical host')
|
|
72
|
+
expect(actual[:STDOUT]).to include('Group: prod group')
|
|
73
|
+
expect(actual[:STDOUT]).to include('Hosts: web 01')
|
|
74
|
+
expect(@db.models[:audit_event].count).to eq(before_audit_count)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'reports command-specific usage for extra or invalid arguments' do
|
|
78
|
+
actual = run_console("host one two\ntags host\naudit nope\naudit 0\nhelp extra\nquit\n")
|
|
79
|
+
|
|
80
|
+
expect(actual[:unexpected]).to eq(false)
|
|
81
|
+
expect(actual[:STDOUT]).to include('Usage: host NAME')
|
|
82
|
+
expect(actual[:STDOUT]).to include('Usage: tags host|group NAME')
|
|
83
|
+
expect(actual[:STDOUT].scan('Usage: audit [LIMIT]').length).to eq(2)
|
|
84
|
+
expect(actual[:STDOUT]).to include('Usage: help')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'reports unclosed quoted input without leaving read-only mode' do
|
|
88
|
+
before_audit_count = @db.models[:audit_event].count
|
|
89
|
+
|
|
90
|
+
actual = run_console("host \"unterminated\nquit\n")
|
|
91
|
+
|
|
92
|
+
expect(actual[:unexpected]).to eq(false)
|
|
93
|
+
expect(actual[:STDOUT]).to include('Invalid command syntax:')
|
|
94
|
+
expect(actual[:STDOUT]).to include('Goodbye.')
|
|
95
|
+
expect(@db.models[:audit_event].count).to eq(before_audit_count)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
# rubocop:enable Metrics/BlockLength
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'cli/factory'
|
|
5
|
+
require 'operations/query_inventory'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moose::Inventory::Cli::Factory do
|
|
8
|
+
let(:context) { instance_double('InventoryContext') }
|
|
9
|
+
subject(:factory) { described_class.new(context: context) }
|
|
10
|
+
|
|
11
|
+
it 'builds operations with the shared context' do
|
|
12
|
+
operation_class = class_double('OperationClass')
|
|
13
|
+
operation = instance_double('Operation')
|
|
14
|
+
|
|
15
|
+
expect(operation_class).to receive(:new).with(context: context, emitter: :emit).and_return(operation)
|
|
16
|
+
|
|
17
|
+
expect(factory.operation(operation_class, emitter: :emit)).to eq(operation)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'memoizes the query inventory wrapper for the shared context' do
|
|
21
|
+
first = factory.query_inventory
|
|
22
|
+
second = factory.query_inventory
|
|
23
|
+
|
|
24
|
+
expect(first).to be_a(Moose::Inventory::Operations::QueryInventory)
|
|
25
|
+
expect(second).to equal(first)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
|
|
3
|
-
# TODO: the usual respond_to? method doesn't seem to work on Thor objects.
|
|
4
5
|
# Why not? For now, we'll check against instance_methods.
|
|
5
6
|
|
|
6
7
|
RSpec.describe Moose::Inventory::Cli::Formatter do
|
|
@@ -33,7 +34,7 @@ RSpec.describe Moose::Inventory::Cli::Formatter do
|
|
|
33
34
|
actual = runner { @formatter.out(test, 'json') }
|
|
34
35
|
|
|
35
36
|
desired = { aborted: false, STDOUT: '', STDERR: '' }
|
|
36
|
-
desired[:STDOUT] = test.to_json
|
|
37
|
+
desired[:STDOUT] = "#{test.to_json}\n"
|
|
37
38
|
|
|
38
39
|
expected(actual, desired)
|
|
39
40
|
end
|
|
@@ -44,7 +45,7 @@ RSpec.describe Moose::Inventory::Cli::Formatter do
|
|
|
44
45
|
actual = runner { @formatter.out(test, 'prettyjson') }
|
|
45
46
|
|
|
46
47
|
desired = { aborted: false, STDOUT: '', STDERR: '' }
|
|
47
|
-
desired[:STDOUT] = JSON.pretty_generate(test)
|
|
48
|
+
desired[:STDOUT] = "#{JSON.pretty_generate(test)}\n"
|
|
48
49
|
|
|
49
50
|
expected(actual, desired)
|
|
50
51
|
end
|
|
@@ -59,5 +60,96 @@ RSpec.describe Moose::Inventory::Cli::Formatter do
|
|
|
59
60
|
|
|
60
61
|
expected(actual, desired)
|
|
61
62
|
end
|
|
63
|
+
|
|
64
|
+
it 'info() prints the provided message instead of a literal placeholder' do
|
|
65
|
+
actual = runner { @formatter.info(2, 'hello world') }
|
|
66
|
+
|
|
67
|
+
desired = { aborted: false, STDOUT: '', STDERR: '' }
|
|
68
|
+
desired[:STDOUT] = ' INFO: hello world'
|
|
69
|
+
|
|
70
|
+
expected(actual, desired)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe 'stream helpers' do
|
|
75
|
+
it 'puts() writes indented text to stdout by default' do
|
|
76
|
+
actual = runner { @formatter.puts(2, 'hello world') }
|
|
77
|
+
|
|
78
|
+
desired = { aborted: false, STDOUT: " hello world\n", STDERR: '' }
|
|
79
|
+
|
|
80
|
+
expected(actual, desired)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'puts() writes indented text to stderr when requested' do
|
|
84
|
+
actual = runner { @formatter.puts(2, 'hello world', 'STDERR') }
|
|
85
|
+
|
|
86
|
+
desired = { aborted: false, STDOUT: '', STDERR: " hello world\n" }
|
|
87
|
+
|
|
88
|
+
expected(actual, desired)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'puts() aborts on an unknown output stream' do
|
|
92
|
+
actual = runner { @formatter.puts(2, 'hello world', 'BOGUS') }
|
|
93
|
+
|
|
94
|
+
desired = { aborted: true, STDOUT: '', STDERR: "Output stream 'BOGUS' is not known.\n" }
|
|
95
|
+
|
|
96
|
+
expected(actual, desired)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'print() writes indented text to stdout by default' do
|
|
100
|
+
actual = runner { @formatter.print(2, 'hello world') }
|
|
101
|
+
|
|
102
|
+
desired = { aborted: false, STDOUT: ' hello world', STDERR: '' }
|
|
103
|
+
|
|
104
|
+
expected(actual, desired)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'print() writes indented text to stderr when requested' do
|
|
108
|
+
actual = runner { @formatter.print(2, 'hello world', 'STDERR') }
|
|
109
|
+
|
|
110
|
+
desired = { aborted: false, STDOUT: '', STDERR: ' hello world' }
|
|
111
|
+
|
|
112
|
+
expected(actual, desired)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'print() aborts on an unknown output stream' do
|
|
116
|
+
actual = runner { @formatter.print(2, 'hello world', 'BOGUS') }
|
|
117
|
+
|
|
118
|
+
desired = { aborted: true, STDOUT: '', STDERR: "Output stream 'BOGUS' is not known.\n" }
|
|
119
|
+
|
|
120
|
+
expected(actual, desired)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'info() writes to stderr when requested' do
|
|
124
|
+
actual = runner { @formatter.info(2, 'hello world', 'STDERR') }
|
|
125
|
+
|
|
126
|
+
desired = { aborted: false, STDOUT: '', STDERR: ' INFO: hello world' }
|
|
127
|
+
|
|
128
|
+
expected(actual, desired)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'info() aborts on an unknown output stream' do
|
|
132
|
+
actual = runner { @formatter.info(2, 'hello world', 'BOGUS') }
|
|
133
|
+
|
|
134
|
+
desired = { aborted: true, STDOUT: '', STDERR: "Output stream 'BOGUS' is not known.\n" }
|
|
135
|
+
|
|
136
|
+
expected(actual, desired)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'warn() writes to stderr with the warning prefix' do
|
|
140
|
+
actual = runner { @formatter.warn('hello world') }
|
|
141
|
+
|
|
142
|
+
desired = { aborted: false, STDOUT: '', STDERR: 'WARNING: hello world' }
|
|
143
|
+
|
|
144
|
+
expected(actual, desired)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'error() writes to stderr with the error prefix' do
|
|
148
|
+
actual = runner { @formatter.error('hello world') }
|
|
149
|
+
|
|
150
|
+
desired = { aborted: false, STDOUT: '', STDERR: 'ERROR: hello world' }
|
|
151
|
+
|
|
152
|
+
expected(actual, desired)
|
|
153
|
+
end
|
|
62
154
|
end
|
|
63
155
|
end
|