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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +21 -0
  5. data/BACKLOG.md +630 -8
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +1 -1
  8. data/README.md +315 -39
  9. data/Rakefile +2 -0
  10. data/bin/moose-inventory +2 -1
  11. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  12. data/docs/compatibility/cli-output-compatibility.md +76 -0
  13. data/docs/governance/approval-register.md +37 -0
  14. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  15. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  16. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  17. data/docs/product/product-brief.md +161 -0
  18. data/docs/product/requirements-baseline.md +477 -0
  19. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  20. data/docs/release/package-provenance-hardening.md +126 -0
  21. data/docs/release/publishing.md +11 -3
  22. data/docs/release/release-environment-protection.md +70 -0
  23. data/docs/release/release-readiness.md +23 -4
  24. data/docs/security/accepted-risk-register.md +84 -0
  25. data/docs/security/security-privacy-process.md +287 -0
  26. data/docs/security-audit-2026-05-26-rerun.md +2 -2
  27. data/docs/ux/cli-workflow-notes.md +287 -0
  28. data/examples/ansible/ansible.cfg +3 -0
  29. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  30. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  31. data/examples/ci/README.md +16 -0
  32. data/examples/ci/github-actions/inventory-review.yml +38 -0
  33. data/examples/ci/inventory/example-snapshot.yml +19 -0
  34. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  35. data/lib/moose_inventory/cli/application.rb +133 -5
  36. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  37. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  38. data/lib/moose_inventory/cli/audit.rb +62 -0
  39. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  40. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  41. data/lib/moose_inventory/cli/console.rb +135 -0
  42. data/lib/moose_inventory/cli/db.rb +64 -0
  43. data/lib/moose_inventory/cli/factory.rb +28 -0
  44. data/lib/moose_inventory/cli/formatter.rb +8 -12
  45. data/lib/moose_inventory/cli/group.rb +5 -2
  46. data/lib/moose_inventory/cli/group_add.rb +11 -9
  47. data/lib/moose_inventory/cli/group_addchild.rb +23 -65
  48. data/lib/moose_inventory/cli/group_addhost.rb +16 -67
  49. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  50. data/lib/moose_inventory/cli/group_get.rb +8 -42
  51. data/lib/moose_inventory/cli/group_list.rb +7 -40
  52. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  53. data/lib/moose_inventory/cli/group_rm.rb +12 -10
  54. data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
  55. data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
  56. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  57. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  58. data/lib/moose_inventory/cli/helpers.rb +68 -1
  59. data/lib/moose_inventory/cli/host.rb +6 -3
  60. data/lib/moose_inventory/cli/host_add.rb +69 -29
  61. data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
  62. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  63. data/lib/moose_inventory/cli/host_get.rb +9 -37
  64. data/lib/moose_inventory/cli/host_list.rb +24 -21
  65. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  66. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  67. data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
  68. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  69. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  70. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  71. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  72. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  73. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  74. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  75. data/lib/moose_inventory/config/config.rb +185 -108
  76. data/lib/moose_inventory/db/db.rb +170 -195
  77. data/lib/moose_inventory/db/exceptions.rb +6 -3
  78. data/lib/moose_inventory/db/models.rb +16 -0
  79. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  80. data/lib/moose_inventory/inventory_context.rb +68 -2
  81. data/lib/moose_inventory/operations/add_associations.rb +20 -16
  82. data/lib/moose_inventory/operations/add_groups.rb +21 -13
  83. data/lib/moose_inventory/operations/add_hosts.rb +30 -17
  84. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  85. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  86. data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
  87. data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
  88. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  89. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  90. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  91. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  92. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  94. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  95. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  96. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  97. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  98. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  99. data/lib/moose_inventory/operations/remove_associations.rb +30 -18
  100. data/lib/moose_inventory/operations/remove_groups.rb +12 -12
  101. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  102. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  103. data/lib/moose_inventory/runtime_options.rb +31 -0
  104. data/lib/moose_inventory/version.rb +3 -1
  105. data/lib/moose_inventory.rb +10 -7
  106. data/moose-inventory.gemspec +19 -35
  107. data/scripts/check.sh +1 -0
  108. data/scripts/ci/check_generated_artifacts.sh +41 -0
  109. data/scripts/ci/check_permissions.sh +2 -0
  110. data/scripts/ci/check_rubocop.sh +30 -25
  111. data/scripts/files.rb +5 -4
  112. data/spec/examples/ci_examples_spec.rb +37 -0
  113. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  114. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  115. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  116. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  117. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  118. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  119. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  120. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  121. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  122. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  123. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  124. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  125. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  126. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  127. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  128. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  129. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
  130. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
  131. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  132. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  133. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  134. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  135. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  136. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  137. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  138. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  139. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  140. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  141. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  142. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  143. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  144. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  145. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  146. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  147. data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
  148. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  149. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  150. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  151. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  152. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
  153. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
  154. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
  155. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  156. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
  157. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  158. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  159. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  160. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  161. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  162. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
  163. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
  164. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  165. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  166. data/spec/shared/shared_config_setup.rb +4 -3
  167. data/spec/spec_helper.rb +50 -40
  168. data/spec/support/cli_harness.rb +33 -0
  169. metadata +80 -41
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ require 'spec_helper'
5
+ require 'operations/operation_event_support'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::OperationEventSupport do
8
+ subject(:operation) { operation_class.new }
9
+
10
+ let(:operation_class) do
11
+ Class.new do
12
+ include Moose::Inventory::Operations::OperationEventSupport
13
+
14
+ def event(type, payload = {})
15
+ build_event(type, payload)
16
+ end
17
+
18
+ def append_event(events, type, payload = {})
19
+ emit(events, type, payload)
20
+ end
21
+
22
+ def result(events:, warning_count: 0)
23
+ operation_result(events: events, warning_count: warning_count)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe '#build_event' do
29
+ it 'builds structured events with an empty default payload' do
30
+ event = operation.event(:started)
31
+
32
+ expect(event).to be_a(described_class::Event)
33
+ expect(event.type).to eq(:started)
34
+ expect(event.payload).to eq({})
35
+ end
36
+
37
+ it 'preserves the provided event payload' do
38
+ event = operation.event(:created, name: 'testhost')
39
+
40
+ expect(event.type).to eq(:created)
41
+ expect(event.payload).to eq(name: 'testhost')
42
+ end
43
+ end
44
+
45
+ describe '#emit' do
46
+ it 'appends a constructed event to the provided collection' do
47
+ events = []
48
+
49
+ result = operation.append_event(events, :warning, name: 'missinggroup')
50
+ event = events.fetch(0)
51
+
52
+ expect(result).to eq(events)
53
+ expect(event).to be_a(described_class::Event)
54
+ expect(event.type).to eq(:warning)
55
+ expect(event.payload).to eq(name: 'missinggroup')
56
+ end
57
+ end
58
+
59
+ describe '#operation_result' do
60
+ it 'defaults warning_count to zero' do
61
+ events = [operation.event(:ok)]
62
+
63
+ result = operation.result(events: events)
64
+
65
+ expect(result).to be_a(described_class::Result)
66
+ expect(result.events).to eq(events)
67
+ expect(result.warning_count).to eq(0)
68
+ expect(result.warning_count.zero?).to eq(true)
69
+ end
70
+
71
+ it 'preserves an explicit warning_count' do
72
+ result = operation.result(events: [], warning_count: 2)
73
+
74
+ expect(result.warning_count).to eq(2)
75
+ end
76
+ end
77
+ end
78
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/query_inventory'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::QueryInventory do
8
+ before(:all) do
9
+ @mockargs = [
10
+ '--config', File.join(spec_root, 'config/config.yml'),
11
+ '--format', 'yaml',
12
+ '--env', 'test'
13
+ ]
14
+
15
+ Moose::Inventory::Config.init(@mockargs)
16
+ @db = Moose::Inventory::DB
17
+ @db.init if @db.db.nil?
18
+ end
19
+
20
+ before(:each) do
21
+ @db.reset
22
+ end
23
+
24
+ def operation
25
+ described_class.new(
26
+ context: Moose::Inventory::InventoryContext.new(db: @db)
27
+ )
28
+ end
29
+
30
+ it 'gets host data without rendering output' do
31
+ host = @db.models[:host].create(name: 'test1')
32
+ host.add_group(@db.models[:group].find_or_create(name: 'ungrouped'))
33
+ var = @db.models[:hostvar].create(name: 'foo', value: 'bar')
34
+ host.add_hostvar(var)
35
+
36
+ actual = runner do
37
+ @result = operation.get_hosts(names: ['test1'])
38
+ end
39
+
40
+ expected(actual, STDOUT: '', STDERR: '')
41
+ expect(@result).to eq(
42
+ test1: {
43
+ groups: ['ungrouped'],
44
+ hostvars: { foo: 'bar' }
45
+ }
46
+ )
47
+ end
48
+
49
+ it 'filters listed hosts by group, tag, and variable' do
50
+ host = @db.models[:host].create(name: 'web01')
51
+ other = @db.models[:host].create(name: 'db01')
52
+ group = @db.models[:group].find_or_create(name: 'web')
53
+ tag = @db.models[:tag].find_or_create(name: 'prod')
54
+ host.add_group(group)
55
+ host.add_tag(tag)
56
+ other.add_tag(tag)
57
+ hostvar = @db.models[:hostvar].create(name: 'os', value: 'fedora')
58
+ other_var = @db.models[:hostvar].create(name: 'os', value: 'debian')
59
+ host.add_hostvar(hostvar)
60
+ other.add_hostvar(other_var)
61
+
62
+ expect(operation.list_hosts(filters: { groups: ['web'], tags: ['prod'], variables: { 'os' => 'fedora' } })).to eq(
63
+ web01: {
64
+ groups: ['web'],
65
+ tags: ['prod'],
66
+ hostvars: { os: 'fedora' }
67
+ }
68
+ )
69
+ end
70
+
71
+ it 'uses DB-backed host filters without loading all hosts first' do
72
+ host = @db.models[:host].create(name: 'web01')
73
+ other = @db.models[:host].create(name: 'db01')
74
+ group = @db.models[:group].find_or_create(name: 'web')
75
+ tag = @db.models[:tag].find_or_create(name: 'prod')
76
+ host.add_group(group)
77
+ host.add_tag(tag)
78
+ other.add_tag(tag)
79
+ host.add_hostvar(@db.models[:hostvar].create(name: 'os', value: 'fedora'))
80
+ other.add_hostvar(@db.models[:hostvar].create(name: 'os', value: 'debian'))
81
+ context = Moose::Inventory::InventoryContext.new(db: @db)
82
+ expect(context).not_to receive(:all_hosts)
83
+
84
+ result = described_class.new(context: context).list_hosts(
85
+ filters: { groups: ['web'], tags: ['prod'], variables: { 'os' => 'fedora' } }
86
+ )
87
+
88
+ expect(result.keys).to eq([:web01])
89
+ end
90
+
91
+ it 'returns no listed hosts when a DB-backed filter references a missing group or tag' do
92
+ @db.models[:host].create(name: 'web01')
93
+
94
+ expect(operation.list_hosts(filters: { groups: ['missing'] })).to eq({})
95
+ expect(operation.list_hosts(filters: { tags: ['missing'] })).to eq({})
96
+ end
97
+
98
+ it 'applies multiple group filters as AND predicates' do
99
+ host = @db.models[:host].create(name: 'web01')
100
+ partial = @db.models[:host].create(name: 'web02')
101
+ web = @db.models[:group].find_or_create(name: 'web')
102
+ prod = @db.models[:group].find_or_create(name: 'prod')
103
+ host.add_group(web)
104
+ host.add_group(prod)
105
+ partial.add_group(web)
106
+
107
+ expect(operation.list_hosts(filters: { groups: %w[web prod] }).keys).to eq([:web01])
108
+ end
109
+
110
+ it 'gets group data while omitting empty relationship collections' do
111
+ @db.models[:group].create(name: 'group1')
112
+
113
+ expect(operation.get_groups(names: ['group1'])).to eq(
114
+ group1: {}
115
+ )
116
+ end
117
+
118
+ it 'lists groups in ansible mode with hosts arrays and vars key' do
119
+ group = @db.models[:group].create(name: 'group1')
120
+ var = @db.models[:groupvar].create(name: 'foo', value: 'bar')
121
+ group.add_groupvar(var)
122
+
123
+ expect(operation.list_groups(ansible: true)).to eq(
124
+ group1: {
125
+ hosts: [],
126
+ vars: { foo: 'bar' }
127
+ }
128
+ )
129
+ end
130
+
131
+ it 'builds ansible hostvars metadata for host listvars queries' do
132
+ host = @db.models[:host].create(name: 'test1')
133
+ host.add_group(@db.models[:group].find_or_create(name: 'ungrouped'))
134
+ var = @db.models[:hostvar].create(name: 'foo', value: 'bar')
135
+ host.add_hostvar(var)
136
+
137
+ expect(operation.list_host_vars(names: ['test1'], ansible: true)).to eq(
138
+ foo: 'bar',
139
+ _meta: {
140
+ hostvars: {
141
+ test1: { foo: 'bar' }
142
+ }
143
+ }
144
+ )
145
+ end
146
+ end
@@ -50,6 +50,41 @@ RSpec.describe Moose::Inventory::Operations::RemoveAssociations do
50
50
  expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
51
51
  end
52
52
 
53
+ it 'dry-runs removing groups from a host without removing associations or adding ungrouped' do
54
+ host = @db.models[:host].create(name: 'host1')
55
+ group = @db.models[:group].create(name: 'group1')
56
+ host.add_group(group)
57
+
58
+ result = operation.host_from_groups(
59
+ host: host,
60
+ host_name: 'host1',
61
+ group_names: ['group1'],
62
+ dry_run: true
63
+ )
64
+
65
+ expect(result.warning_count).to eq(0)
66
+ expect(result.events.map(&:type)).to include(:adding_automatic_group, :dry_run_summary)
67
+ expect(host.groups_dataset[name: 'group1']).not_to be_nil
68
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
69
+ end
70
+
71
+ it 'dry-runs removing hosts from a group without removing associations or adding ungrouped' do
72
+ group = @db.models[:group].create(name: 'group1')
73
+ host = @db.models[:host].create(name: 'host1')
74
+ group.add_host(host)
75
+
76
+ result = operation.group_from_hosts(
77
+ group: group,
78
+ group_name: 'group1',
79
+ host_names: ['host1'],
80
+ dry_run: true
81
+ )
82
+
83
+ expect(result.warning_count).to eq(0)
84
+ expect(result.events.map(&:type)).to include(:adding_automatic_group, :dry_run_summary)
85
+ expect(group.hosts_dataset[name: 'host1']).not_to be_nil
86
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
87
+ end
53
88
  it 'removes hosts from a group and reports missing associations and ungrouped reattachment' do
54
89
  group = @db.models[:group].create(name: 'group1')
55
90
  host1 = @db.models[:host].create(name: 'host1')
@@ -34,6 +34,27 @@ RSpec.describe Moose::Inventory::Operations::RemoveGroups do
34
34
  expect(result.events.map(&:type)).to include(:group_missing)
35
35
  end
36
36
 
37
+ it 'returns dry-run events without removing groups or recursive children' do
38
+ parent = @db.models[:group].create(name: 'parent')
39
+ child = @db.models[:group].create(name: 'child')
40
+ host = @db.models[:host].create(name: 'child-host')
41
+ child.add_host(host)
42
+ parent.add_child(child)
43
+
44
+ result = operation.call(names: ['parent'], recursive: true, dry_run: true)
45
+
46
+ expect(result.warning_count).to eq(0)
47
+ expect(result.events.map(&:type)).to include(
48
+ :removing_child_association,
49
+ :recursively_delete_orphaned_group,
50
+ :adding_automatic_group_to_host,
51
+ :destroying_group,
52
+ :dry_run_summary
53
+ )
54
+ expect(@db.models[:group].find(name: 'parent')).not_to be_nil
55
+ expect(@db.models[:group].find(name: 'child')).not_to be_nil
56
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
57
+ end
37
58
  it 'removes groups and recursively cleans orphaned children when requested' do
38
59
  parent = @db.models[:group].create(name: 'parent')
39
60
  child = @db.models[:group].create(name: 'child')
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Moose::Inventory::Operations::RemoveHosts do
6
+ before(:all) do
7
+ mockargs = [
8
+ '--config', File.join(spec_root, 'config/config.yml'),
9
+ '--format', 'yaml',
10
+ '--env', 'test'
11
+ ]
12
+
13
+ @config = Moose::Inventory::Config
14
+ @config.init(mockargs)
15
+
16
+ @db = Moose::Inventory::DB
17
+ @db.init if @db.db.nil?
18
+ end
19
+
20
+ before(:each) do
21
+ @db.reset
22
+ @context = Moose::Inventory::InventoryContext.new(db: @db)
23
+ @operation = described_class.new(context: @context)
24
+ end
25
+
26
+ it 'removes an existing host' do
27
+ host = @db.models[:host].create(name: 'test1')
28
+ host.add_group(@db.models[:group].find_or_create(name: 'ungrouped'))
29
+
30
+ result = @operation.call(names: ['test1'])
31
+
32
+ expect(result.warning_count).to eq(0)
33
+ expect(@db.models[:host].find(name: 'test1')).to be_nil
34
+ expect(result.events.map(&:type)).to eq(%i[host_started retrieving_host ok destroying_host ok host_complete])
35
+ end
36
+
37
+ it 'returns dry-run events without deleting an existing host' do
38
+ host = @db.models[:host].create(name: 'test1')
39
+ host.add_group(@db.models[:group].find_or_create(name: 'ungrouped'))
40
+
41
+ result = @operation.call(names: ['test1'], dry_run: true)
42
+
43
+ expect(result.warning_count).to eq(0)
44
+ expect(@db.models[:host].find(name: 'test1')).not_to be_nil
45
+ expect(result.events.map(&:type)).to eq(%i[host_started retrieving_host ok destroying_host ok host_complete
46
+ dry_run_summary])
47
+ end
48
+ it 'returns a warning event when the host does not exist' do
49
+ result = @operation.call(names: ['fake'])
50
+
51
+ expect(result.warning_count).to eq(1)
52
+ expect(result.events.map(&:type)).to eq(%i[host_started retrieving_host host_missing missing_skipping ok
53
+ host_complete])
54
+ end
55
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/remove_variables'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::RemoveVariables do
8
+ before(:all) do
9
+ @mockargs = [
10
+ '--config', File.join(spec_root, 'config/config.yml'),
11
+ '--format', 'yaml',
12
+ '--env', 'test'
13
+ ]
14
+
15
+ Moose::Inventory::Config.init(@mockargs)
16
+ @db = Moose::Inventory::DB
17
+ @db.init if @db.db.nil?
18
+ end
19
+
20
+ before(:each) do
21
+ @db.reset
22
+ end
23
+
24
+ def build_operation(entity_type, emitter: nil)
25
+ described_class.new(
26
+ context: Moose::Inventory::InventoryContext.new(db: @db),
27
+ entity_type: entity_type,
28
+ emitter: emitter
29
+ )
30
+ end
31
+
32
+ it 'removes host variables and returns structured events without rendering output' do
33
+ host = @db.models[:host].create(name: 'test1')
34
+ var = @db.models[:hostvar].create(name: 'var1', value: 'val1')
35
+ host.add_hostvar(var)
36
+
37
+ actual = runner do
38
+ @result = build_operation(:host).call(name: 'test1', vars: ['var1'])
39
+ end
40
+
41
+ expected(actual, STDOUT: '', STDERR: '')
42
+ expect(@result.events.map(&:type)).to eq(
43
+ %i[entity_started retrieving_entity ok removing_variable ok entity_complete]
44
+ )
45
+ expect(host.hostvars_dataset.count).to eq(0)
46
+ expect(@db.models[:hostvar].count).to eq(0)
47
+ end
48
+
49
+ it 'accepts group variable removal by key=value syntax and removes the record' do
50
+ group = @db.models[:group].create(name: 'testgroup')
51
+ var = @db.models[:groupvar].create(name: 'var1', value: 'val1')
52
+ group.add_groupvar(var)
53
+
54
+ build_operation(:group).call(name: 'testgroup', vars: ['var1=val1'])
55
+
56
+ expect(group.groupvars_dataset.count).to eq(0)
57
+ expect(@db.models[:groupvar].count).to eq(0)
58
+ end
59
+
60
+ it 'dry-runs removing variables without deleting records' do
61
+ host = @db.models[:host].create(name: 'test1')
62
+ var = @db.models[:hostvar].create(name: 'var1', value: 'val1')
63
+ host.add_hostvar(var)
64
+
65
+ result = build_operation(:host).call(name: 'test1', vars: ['var1'], dry_run: true)
66
+
67
+ expect(result.events.map(&:type)).to include(:removing_variable, :dry_run_summary)
68
+ expect(host.hostvars_dataset[name: 'var1']).not_to be_nil
69
+ expect(@db.models[:hostvar].count).to eq(1)
70
+ end
71
+
72
+ it 'emits partial progress before raising on malformed group variable removal input' do
73
+ @db.models[:group].create(name: 'testgroup')
74
+ emitted = []
75
+ operation = build_operation(:group, emitter: emitted.method(:<<))
76
+
77
+ expect do
78
+ operation.call(name: 'testgroup', vars: ['=broken'])
79
+ end.to raise_error(Moose::Inventory::DB.exceptions[:moose], /Expected 'key' or 'key=value'/)
80
+
81
+ expect(emitted.map(&:type)).to eq(%i[entity_started retrieving_entity ok removing_variable])
82
+ end
83
+ end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  RSpec.shared_context 'shared config init', a: :b do
3
4
  before(:all) do
4
5
  @mockarg_parts = {
5
- config: File.join(spec_dir, 'config/config.yml'),
6
- format: 'yaml',
7
- env: 'test',
6
+ config: File.join(spec_dir, 'config/config.yml'),
7
+ format: 'yaml',
8
+ env: 'test'
8
9
  }
9
10
 
10
11
  @mockargs = []
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if RUBY_VERSION >= '1.9'
2
4
  require 'simplecov'
3
5
  SimpleCov.formatters = [
@@ -31,34 +33,34 @@ require 'yaml'
31
33
  require 'fileutils'
32
34
  require 'find'
33
35
  require 'moose_inventory'
36
+ require_relative 'support/cli_harness'
34
37
 
35
- RSpec.configure do |config|
36
- # config.filter_run focus: true # <- enable to allow test focus
37
- config.color = true
38
- config.tty = true
39
- config.formatter = :progress #:documentation # :progress, :html, :textmate
40
- def capture(stream)
38
+ module SpecOutputHelpers
39
+ def capture(stream, &)
41
40
  case stream
42
41
  when :STDOUT
43
- begin
44
- orig = $stdout
45
- $stdout = StringIO.new
46
- yield
47
- result = $stdout.string
48
- ensure
49
- $stdout = orig
50
- end
42
+ capture_stdout(&)
51
43
  when :STDERR
52
- begin
53
- orig = $stderr
54
- $stderr = StringIO.new
55
- yield
56
- result = $stderr.string
57
- ensure
58
- $stderr = orig
59
- end
44
+ capture_stderr(&)
60
45
  end
61
- result
46
+ end
47
+
48
+ def capture_stdout
49
+ orig = $stdout
50
+ $stdout = StringIO.new
51
+ yield
52
+ $stdout.string
53
+ ensure
54
+ $stdout = orig
55
+ end
56
+
57
+ def capture_stderr
58
+ orig = $stderr
59
+ $stderr = StringIO.new
60
+ yield
61
+ $stderr.string
62
+ ensure
63
+ $stderr = orig
62
64
  end
63
65
 
64
66
  def runner
@@ -66,17 +68,13 @@ RSpec.configure do |config|
66
68
 
67
69
  out[:STDERR] = capture(:STDERR) do
68
70
  out[:STDOUT] = capture(:STDOUT) do
69
- begin
70
- yield
71
-
72
- rescue SystemExit
73
- out[:aborted] = true
74
-
75
- # rubocop:disable Lint/RescueException
76
- rescue Exception => e
77
- # rubocop:enable Lint/RescueException
78
- out[:unexpected] = e
79
- end
71
+ yield
72
+ rescue SystemExit
73
+ out[:aborted] = true
74
+ # rubocop:disable Lint/RescueException
75
+ rescue Exception => e
76
+ # rubocop:enable Lint/RescueException
77
+ out[:unexpected] = e
80
78
  end
81
79
  end
82
80
  out # return the output
@@ -93,17 +91,29 @@ RSpec.configure do |config|
93
91
  expect(actual[:STDERR]).to eq(desired[:STDERR])
94
92
  end
95
93
 
96
- def clobber_db_files
94
+ def spec_root
95
+ File.dirname(__FILE__)
96
+ end
97
+ end
98
+
99
+ module SpecDatabaseHelpers
100
+ def self.clobber_db_files
97
101
  FileUtils.mkdir_p('tmp')
98
102
  paths = []
99
103
  Find.find('tmp') { |path| paths << path if path =~ /.*\.db$/ }
100
104
  paths.each { |file| File.delete(file) }
101
105
  end
106
+ end
102
107
 
103
- def spec_root
104
- File.dirname(__FILE__)
105
- end
108
+ RSpec.configure do |config|
109
+ config.include CliHarness
110
+ config.include SpecOutputHelpers
111
+ # config.filter_run focus: true # <- enable to allow test focus
112
+ config.color = true
113
+ config.tty = true
114
+ config.formatter = :progress # :documentation # :progress, :html, :textmate
106
115
 
107
- # Always start with a fresh db file.
108
- clobber_db_files
116
+ config.before(:suite) do
117
+ SpecDatabaseHelpers.clobber_db_files
118
+ end
109
119
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CliHarness
4
+ def setup_cli_harness(command_class:, command_ivar: nil, include_cli: false, extra_commands: {}, extra_args: [])
5
+ @mockarg_parts = {
6
+ config: File.join(spec_root, 'config/config.yml'),
7
+ format: 'yaml',
8
+ env: 'test'
9
+ }
10
+ @mockargs = build_cli_args(@mockarg_parts) + extra_args
11
+
12
+ @config = Moose::Inventory::Config
13
+ @config.init(@mockargs)
14
+
15
+ @console = Moose::Inventory::Cli::Formatter
16
+ @db = Moose::Inventory::DB
17
+ @db.init if @db.db.nil?
18
+
19
+ @app = Moose::Inventory::Cli::Application
20
+ @cli = Moose::Inventory::Cli if include_cli
21
+ instance_variable_set(command_ivar, command_class) unless command_ivar.nil?
22
+ extra_commands.each { |ivar, klass| instance_variable_set(ivar, klass) }
23
+ end
24
+
25
+ def reset_cli_harness(reset_config: false)
26
+ @config.init(@mockargs) if reset_config
27
+ @db.reset
28
+ end
29
+
30
+ def build_cli_args(parts)
31
+ parts.flat_map { |key, value| ["--#{key}", value] }
32
+ end
33
+ end