moose-inventory 1.0.9 → 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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +60 -0
  4. data/.gitignore +2 -1
  5. data/.gitleaks.toml +9 -0
  6. data/.rubocop.yml +49 -0
  7. data/BACKLOG.md +752 -24
  8. data/Gemfile +2 -0
  9. data/Gemfile.lock +36 -1
  10. data/README.md +340 -44
  11. data/Rakefile +2 -0
  12. data/bin/moose-inventory +2 -1
  13. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  14. data/docs/compatibility/cli-output-compatibility.md +76 -0
  15. data/docs/governance/approval-register.md +37 -0
  16. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  17. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  18. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  19. data/docs/product/product-brief.md +161 -0
  20. data/docs/product/requirements-baseline.md +477 -0
  21. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  22. data/docs/release/package-provenance-hardening.md +126 -0
  23. data/docs/release/publishing.md +54 -50
  24. data/docs/release/release-environment-protection.md +70 -0
  25. data/docs/release/release-readiness.md +37 -4
  26. data/docs/security/accepted-risk-register.md +84 -0
  27. data/docs/security/security-privacy-process.md +287 -0
  28. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  29. data/docs/security-audit-2026-05-26.md +63 -0
  30. data/docs/ux/cli-workflow-notes.md +287 -0
  31. data/examples/ansible/ansible.cfg +3 -0
  32. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  33. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  34. data/examples/ci/README.md +16 -0
  35. data/examples/ci/github-actions/inventory-review.yml +38 -0
  36. data/examples/ci/inventory/example-snapshot.yml +19 -0
  37. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  38. data/lib/moose_inventory/cli/application.rb +133 -5
  39. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  40. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  41. data/lib/moose_inventory/cli/audit.rb +62 -0
  42. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  43. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  44. data/lib/moose_inventory/cli/console.rb +135 -0
  45. data/lib/moose_inventory/cli/db.rb +64 -0
  46. data/lib/moose_inventory/cli/factory.rb +28 -0
  47. data/lib/moose_inventory/cli/formatter.rb +8 -12
  48. data/lib/moose_inventory/cli/group.rb +7 -1
  49. data/lib/moose_inventory/cli/group_add.rb +91 -73
  50. data/lib/moose_inventory/cli/group_addchild.rb +41 -66
  51. data/lib/moose_inventory/cli/group_addhost.rb +33 -71
  52. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  53. data/lib/moose_inventory/cli/group_get.rb +8 -42
  54. data/lib/moose_inventory/cli/group_list.rb +7 -40
  55. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  56. data/lib/moose_inventory/cli/group_rm.rb +105 -73
  57. data/lib/moose_inventory/cli/group_rmchild.rb +47 -57
  58. data/lib/moose_inventory/cli/group_rmhost.rb +34 -61
  59. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  60. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  61. data/lib/moose_inventory/cli/helpers.rb +143 -0
  62. data/lib/moose_inventory/cli/host.rb +8 -2
  63. data/lib/moose_inventory/cli/host_add.rb +91 -66
  64. data/lib/moose_inventory/cli/host_addgroup.rb +39 -66
  65. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  66. data/lib/moose_inventory/cli/host_get.rb +9 -37
  67. data/lib/moose_inventory/cli/host_list.rb +24 -21
  68. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  69. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  70. data/lib/moose_inventory/cli/host_rmgroup.rb +39 -55
  71. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  72. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  73. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  74. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  75. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  76. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  77. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  78. data/lib/moose_inventory/config/config.rb +185 -108
  79. data/lib/moose_inventory/db/db.rb +188 -193
  80. data/lib/moose_inventory/db/exceptions.rb +6 -3
  81. data/lib/moose_inventory/db/models.rb +16 -0
  82. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  83. data/lib/moose_inventory/inventory_context.rb +116 -0
  84. data/lib/moose_inventory/operations/add_associations.rb +131 -0
  85. data/lib/moose_inventory/operations/add_groups.rb +123 -0
  86. data/lib/moose_inventory/operations/add_hosts.rb +123 -0
  87. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  88. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  89. data/lib/moose_inventory/operations/group_child_relations.rb +125 -0
  90. data/lib/moose_inventory/operations/group_cleanup.rb +70 -0
  91. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  92. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  95. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  96. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  97. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  98. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  99. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  100. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  101. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  102. data/lib/moose_inventory/operations/remove_associations.rb +113 -0
  103. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  104. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  105. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  106. data/lib/moose_inventory/runtime_options.rb +31 -0
  107. data/lib/moose_inventory/version.rb +3 -1
  108. data/lib/moose_inventory.rb +10 -7
  109. data/moose-inventory.gemspec +22 -35
  110. data/scripts/check.sh +3 -0
  111. data/scripts/ci/check_generated_artifacts.sh +41 -0
  112. data/scripts/ci/check_permissions.sh +5 -0
  113. data/scripts/ci/check_rubocop.sh +33 -0
  114. data/scripts/ci/check_secrets.sh +26 -0
  115. data/scripts/ci/check_security.sh +18 -0
  116. data/scripts/ci/install_security_tools.sh +47 -0
  117. data/scripts/files.rb +5 -4
  118. data/scripts/install_dependencies.sh +2 -0
  119. data/spec/examples/ci_examples_spec.rb +37 -0
  120. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  121. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  122. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  123. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  124. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  125. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  126. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  127. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  128. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  129. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  130. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  131. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  132. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  133. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  134. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  135. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  136. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +165 -85
  137. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +100 -30
  138. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  139. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  140. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  141. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  142. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  143. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  144. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  145. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  146. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  147. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  148. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  149. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  150. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  151. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  152. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  153. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  154. data/spec/lib/moose_inventory/db/db_spec.rb +551 -29
  155. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  156. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  157. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  158. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  159. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +111 -0
  160. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +80 -0
  161. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +82 -0
  162. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  163. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +122 -0
  164. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  165. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  166. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  167. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  168. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  169. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +113 -0
  170. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +78 -0
  171. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  172. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  173. data/spec/shared/shared_config_setup.rb +4 -3
  174. data/spec/spec_helper.rb +50 -40
  175. data/spec/support/cli_harness.rb +33 -0
  176. metadata +163 -35
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Moose::Inventory::DB::MooseDBException do
6
+ it 'uses the provided exception message through RuntimeError' do
7
+ error = described_class.new('boom')
8
+
9
+ expect(error.message).to eq('boom')
10
+ expect(error.full_message).to include('boom')
11
+ end
12
+
13
+ it 'falls back to a default exception message' do
14
+ error = described_class.new
15
+
16
+ expect(error.message).to eq('An undefined Moose exception occurred')
17
+ end
18
+ end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
5
+ # rubocop:disable Metrics/BlockLength
3
6
  RSpec.describe 'models' do
4
7
  #=============================
5
8
  # Initialization
@@ -8,9 +11,9 @@ RSpec.describe 'models' do
8
11
  before(:all) do
9
12
  # Set up the configuration object
10
13
  @mockarg_parts = {
11
- config: File.join(spec_root, 'config/config.yml'),
12
- format: 'yaml',
13
- env: 'test',
14
+ config: File.join(spec_root, 'config/config.yml'),
15
+ format: 'yaml',
16
+ env: 'test'
14
17
  }
15
18
 
16
19
  @mockargs = []
@@ -173,3 +176,4 @@ RSpec.describe 'models' do
173
176
  end
174
177
  end
175
178
  end
179
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ require 'spec_helper'
5
+ require 'tmpdir'
6
+
7
+ RSpec.describe 'database lifecycle commands' 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
+ it 'records the current schema version during database initialization' do
17
+ expect(Moose::Inventory::DB.schema_version).to eq(Moose::Inventory::DB::SCHEMA_VERSION)
18
+ expect(Moose::Inventory::DB.status[:tables][:schema_info]).to eq(true)
19
+ end
20
+
21
+ it 'prints database status' do
22
+ actual = runner { @app.start(%w[db status]) }
23
+
24
+ expect(actual[:unexpected]).to eq(false)
25
+ expect(actual[:aborted]).to eq(false)
26
+ expect(actual[:STDOUT]).to include('Adapter: sqlite3')
27
+ expect(actual[:STDOUT]).to include("Expected schema version: #{Moose::Inventory::DB::SCHEMA_VERSION}")
28
+ expect(actual[:STDOUT]).to include('- schema_info: present')
29
+ end
30
+
31
+ it 'runs db doctor successfully when schema state is current' do
32
+ actual = runner { @app.start(%w[db doctor]) }
33
+
34
+ expected(actual, aborted: false, STDOUT: "Database doctor found no issues.\n", STDERR: '')
35
+ end
36
+
37
+ it 'reports dirty partial schema state through db doctor' do
38
+ Moose::Inventory::DB.db.drop_table(:audit_events)
39
+
40
+ actual = runner { @app.start(%w[db doctor]) }
41
+
42
+ expect(actual[:unexpected]).to eq(false)
43
+ expect(actual[:aborted]).to eq(true)
44
+ expect(actual[:STDOUT]).to include('Database doctor found issue(s):')
45
+ expect(actual[:STDOUT]).to include('- Missing tables: audit_events')
46
+ end
47
+
48
+ it 'migrates missing lifecycle metadata' do
49
+ Moose::Inventory::DB.db.drop_table(:schema_info)
50
+
51
+ actual = runner { @app.start(%w[db migrate]) }
52
+
53
+ expect(actual[:unexpected]).to eq(false)
54
+ expect(actual[:aborted]).to eq(false)
55
+ expect(actual[:STDOUT]).to include("Database schema is at version #{Moose::Inventory::DB::SCHEMA_VERSION}.")
56
+ expect(Moose::Inventory::DB.schema_version).to eq(Moose::Inventory::DB::SCHEMA_VERSION)
57
+ end
58
+
59
+ it 'backs up the sqlite database file' do
60
+ Dir.mktmpdir do |dir|
61
+ destination = File.join(dir, 'backup.sqlite3')
62
+
63
+ actual = runner { @app.start(['db', 'backup', destination]) }
64
+
65
+ expect(actual[:unexpected]).to eq(false)
66
+ expect(actual[:aborted]).to eq(false)
67
+ expect(actual[:STDOUT]).to include("Backed up database to #{destination}.")
68
+ expect(File).to exist(destination)
69
+ expect(File.size(destination)).to be_positive
70
+ end
71
+ end
72
+ end
73
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+
6
+ RSpec.describe Moose::Inventory::InventoryContext do
7
+ it 'requires an explicit db dependency' do
8
+ expect { described_class.new }.to raise_error(ArgumentError)
9
+ end
10
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/add_associations'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::AddAssociations 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 'adds groups to an existing host and reports creation/duplicate events' do
31
+ host = @db.models[:host].create(name: 'host1')
32
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
33
+ host.add_group(ungrouped)
34
+ existing_group = @db.models[:group].create(name: 'existing')
35
+ host.add_group(existing_group)
36
+
37
+ result = operation.host_to_groups(
38
+ host: host,
39
+ host_name: 'host1',
40
+ group_names: %w[existing created]
41
+ )
42
+
43
+ expect(result.warning_count).to eq(2)
44
+ expect(result.events.map(&:type)).to include(
45
+ :host_group_association_exists,
46
+ :group_missing_created,
47
+ :removing_automatic_group
48
+ )
49
+ expect(host.groups_dataset[name: 'created']).not_to be_nil
50
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
51
+ end
52
+
53
+ it 'dry-runs adding groups to a host without creating groups or changing automatic membership' do
54
+ host = @db.models[:host].create(name: 'host1')
55
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
56
+ host.add_group(ungrouped)
57
+
58
+ result = operation.host_to_groups(
59
+ host: host,
60
+ host_name: 'host1',
61
+ group_names: ['created'],
62
+ dry_run: true
63
+ )
64
+
65
+ expect(result.warning_count).to eq(1)
66
+ expect(result.events.map(&:type)).to include(:group_missing_created, :removing_automatic_group, :dry_run_summary)
67
+ expect(@db.models[:group].find(name: 'created')).to be_nil
68
+ expect(host.groups_dataset[name: 'created']).to be_nil
69
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
70
+ end
71
+
72
+ it 'dry-runs adding hosts to a group without creating hosts or changing automatic membership' do
73
+ group = @db.models[:group].create(name: 'group1')
74
+
75
+ result = operation.group_to_hosts(
76
+ group: group,
77
+ group_name: 'group1',
78
+ host_names: ['host1'],
79
+ dry_run: true
80
+ )
81
+
82
+ expect(result.warning_count).to eq(1)
83
+ expect(result.events.map(&:type)).to include(:host_missing_created, :dry_run_summary)
84
+ expect(@db.models[:host].find(name: 'host1')).to be_nil
85
+ expect(group.hosts_dataset[name: 'host1']).to be_nil
86
+ end
87
+ it 'adds hosts to an existing group and reports creation/duplicate events' do
88
+ group = @db.models[:group].create(name: 'group1')
89
+ duplicate_host = @db.models[:host].create(name: 'host1')
90
+ existing_host = @db.models[:host].create(name: 'host3')
91
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
92
+ duplicate_host.add_group(ungrouped)
93
+ existing_host.add_group(ungrouped)
94
+ group.add_host(duplicate_host)
95
+
96
+ result = operation.group_to_hosts(
97
+ group: group,
98
+ group_name: 'group1',
99
+ host_names: %w[host1 host2 host3]
100
+ )
101
+
102
+ expect(result.warning_count).to eq(2)
103
+ expect(result.events.map(&:type)).to include(
104
+ :group_host_association_exists,
105
+ :host_missing_created,
106
+ :removing_automatic_group
107
+ )
108
+ expect(group.hosts_dataset[name: 'host2']).not_to be_nil
109
+ expect(existing_host.groups_dataset[name: 'ungrouped']).to be_nil
110
+ end
111
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/add_groups'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::AddGroups 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 'adds a group and returns structured events without rendering output' do
31
+ actual = runner do
32
+ @result = operation.call(names: ['testgroup'], hosts: [])
33
+ end
34
+
35
+ expected(actual, STDOUT: '', STDERR: '')
36
+ expect(@result.warning_count).to eq(0)
37
+ expect(@result.events.map(&:type)).to eq(%i[group_started creating_group ok group_complete])
38
+
39
+ group = @db.models[:group].find(name: 'testgroup')
40
+ expect(group).not_to be_nil
41
+ end
42
+
43
+ it 'returns dry-run events without creating groups, hosts, or associations' do
44
+ result = operation.call(
45
+ names: ['testgroup'],
46
+ hosts: ['missinghost'],
47
+ dry_run: true
48
+ )
49
+
50
+ expect(result.warning_count).to eq(1)
51
+ expect(result.events.map(&:type)).to eq(
52
+ %i[group_started creating_group ok adding_association host_missing_created host_creating_now ok ok group_complete
53
+ dry_run_summary]
54
+ )
55
+ expect(@db.models[:group].find(name: 'testgroup')).to be_nil
56
+ expect(@db.models[:host].find(name: 'missinghost')).to be_nil
57
+ end
58
+ it 'reports existing groups, created hosts, duplicate associations, and ungrouped removal as events' do
59
+ host = @db.models[:host].create(name: 'testhost')
60
+ ungrouped = @db.models[:group].find_or_create(name: 'ungrouped')
61
+ host.add_group(ungrouped)
62
+ group = @db.models[:group].create(name: 'testgroup')
63
+ group.add_host(host)
64
+
65
+ @result = operation.call(
66
+ names: ['testgroup'],
67
+ hosts: %w[testhost newhost]
68
+ )
69
+
70
+ expect(@result.warning_count).to eq(3)
71
+ expect(@result.events.map(&:type)).to include(
72
+ :group_exists,
73
+ :association_exists,
74
+ :host_missing_created,
75
+ :removing_automatic_group
76
+ )
77
+ expect(@db.models[:host].find(name: 'newhost')).not_to be_nil
78
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
79
+ end
80
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Moose::Inventory::Operations::AddHosts 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
+ Moose::Inventory::Config.init(@mockargs)
14
+ @db = Moose::Inventory::DB
15
+ @db.init if @db.db.nil?
16
+ end
17
+
18
+ before(:each) do
19
+ @db.reset
20
+ end
21
+
22
+ def operation
23
+ described_class.new(
24
+ context: Moose::Inventory::InventoryContext.new(db: @db)
25
+ )
26
+ end
27
+
28
+ describe '#call' do
29
+ it 'adds a host and returns structured events without rendering output' do
30
+ actual = runner do
31
+ @result = operation.call(names: ['testhost'], groups: [])
32
+ end
33
+
34
+ expected(actual, STDOUT: '', STDERR: '')
35
+ expect(@result.events.map(&:type)).to eq(
36
+ %i[host_started creating_host ok adding_automatic_group ok host_complete]
37
+ )
38
+ expect(@result.events[0].payload).to eq(name: 'testhost')
39
+ expect(@result.events[3].payload).to eq(host: 'testhost', group: 'ungrouped')
40
+
41
+ host = @db.models[:host].find(name: 'testhost')
42
+ expect(host).not_to be_nil
43
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
44
+ end
45
+
46
+ it 'returns dry-run events without creating hosts, groups, or associations' do
47
+ @result = operation.call(
48
+ names: ['testhost'],
49
+ groups: ['missinggroup'],
50
+ dry_run: true
51
+ )
52
+
53
+ expect(@result.events.map(&:type)).to eq(
54
+ %i[host_started creating_host ok adding_association group_missing_created ok host_complete dry_run_summary]
55
+ )
56
+ expect(@db.models[:host].find(name: 'testhost')).to be_nil
57
+ expect(@db.models[:group].find(name: 'missinggroup')).to be_nil
58
+ end
59
+ it 'reports existing hosts, missing groups, and duplicate associations as events' do
60
+ host = @db.models[:host].create(name: 'testhost')
61
+ group = @db.models[:group].create(name: 'existinggroup')
62
+ host.add_group(group)
63
+
64
+ @result = operation.call(
65
+ names: ['testhost'],
66
+ groups: %w[existinggroup missinggroup]
67
+ )
68
+
69
+ expect(@result.events.map(&:type)).to include(
70
+ :host_exists,
71
+ :association_exists,
72
+ :group_missing_created
73
+ )
74
+ expect(@result.events.find { |event| event.type == :host_exists }.payload).to eq(name: 'testhost')
75
+ expect(@result.events.find { |event| event.type == :association_exists }.payload).to eq(
76
+ host: 'testhost',
77
+ group: 'existinggroup'
78
+ )
79
+ expect(@db.models[:group].find(name: 'missinggroup')).not_to be_nil
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/add_variables'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::AddVariables 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 'adds host variables and returns structured events without rendering output' do
33
+ @db.models[:host].create(name: 'test1')
34
+
35
+ actual = runner do
36
+ @result = build_operation(:host).call(name: 'test1', vars: ['var1=val1'])
37
+ end
38
+
39
+ expected(actual, STDOUT: '', STDERR: '')
40
+ expect(@result.events.map(&:type)).to eq(
41
+ %i[entity_started retrieving_entity ok adding_variable ok entity_complete]
42
+ )
43
+
44
+ host = @db.models[:host].find(name: 'test1')
45
+ expect(host.hostvars_dataset[name: 'var1'][:value]).to eq('val1')
46
+ end
47
+
48
+ it 'updates an existing group variable and emits an update event' do
49
+ group = @db.models[:group].create(name: 'testgroup')
50
+ var = @db.models[:groupvar].create(name: 'var1', value: 'old')
51
+ group.add_groupvar(var)
52
+
53
+ result = build_operation(:group).call(name: 'testgroup', vars: ['var1=new'])
54
+
55
+ expect(result.events.map(&:type)).to include(:updating_existing_variable)
56
+ expect(group.groupvars_dataset[name: 'var1'][:value]).to eq('new')
57
+ expect(@db.models[:groupvar].count).to eq(1)
58
+ end
59
+
60
+ it 'dry-runs adding and updating variables without writing records' do
61
+ host = @db.models[:host].create(name: 'test1')
62
+ existing = @db.models[:hostvar].create(name: 'var1', value: 'old')
63
+ host.add_hostvar(existing)
64
+
65
+ result = build_operation(:host).call(
66
+ name: 'test1',
67
+ vars: %w[var1=new var2=val2],
68
+ dry_run: true
69
+ )
70
+
71
+ expect(result.events.map(&:type)).to include(:updating_existing_variable, :dry_run_summary)
72
+ expect(host.hostvars_dataset[name: 'var1'][:value]).to eq('old')
73
+ expect(host.hostvars_dataset[name: 'var2']).to be_nil
74
+ end
75
+
76
+ it 'uses the shared variable operation support for missing entity errors' do
77
+ operation = build_operation(:host)
78
+
79
+ expect do
80
+ operation.call(name: 'ghost', vars: ['var1=val1'])
81
+ end.to raise_error(Moose::Inventory::DB.exceptions[:moose], "The host 'ghost' does not exist.")
82
+ end
83
+
84
+ it 'rejects unsupported entity types via the shared variable operation support' do
85
+ operation = build_operation(:thing)
86
+
87
+ expect do
88
+ operation.call(name: 'whatever', vars: ['var1=val1'])
89
+ end.to raise_error(ArgumentError, /Unsupported entity type/)
90
+ end
91
+
92
+ it 'emits partial progress before raising on malformed host variable input' do
93
+ @db.models[:host].create(name: 'test1')
94
+ emitted = []
95
+ operation = build_operation(:host, emitter: emitted.method(:<<))
96
+
97
+ expect do
98
+ operation.call(name: 'test1', vars: ['broken'])
99
+ end.to raise_error(Moose::Inventory::DB.exceptions[:moose], /Expected 'key=value'/)
100
+
101
+ expect(emitted.map(&:type)).to eq(%i[entity_started retrieving_entity ok adding_variable])
102
+ end
103
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'inventory_context'
5
+ require 'operations/group_child_relations'
6
+
7
+ RSpec.describe Moose::Inventory::Operations::GroupChildRelations 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 'adds child groups and reports duplicate and auto-create events' do
31
+ parent = @db.models[:group].create(name: 'parent')
32
+ existing = @db.models[:group].create(name: 'existing')
33
+ parent.add_child(existing)
34
+
35
+ result = operation.add_children(
36
+ parent_group: parent,
37
+ parent_name: 'parent',
38
+ child_names: %w[existing created]
39
+ )
40
+
41
+ expect(result.warning_count).to eq(2)
42
+ expect(result.events.map(&:type)).to include(
43
+ :child_association_exists,
44
+ :child_group_missing
45
+ )
46
+ expect(parent.children_dataset[name: 'created']).not_to be_nil
47
+ end
48
+
49
+ it 'dry-runs adding child groups without creating children or associations' do
50
+ parent = @db.models[:group].create(name: 'parent')
51
+
52
+ result = operation.add_children(
53
+ parent_group: parent,
54
+ parent_name: 'parent',
55
+ child_names: ['created'],
56
+ dry_run: true
57
+ )
58
+
59
+ expect(result.warning_count).to eq(1)
60
+ expect(result.events.map(&:type)).to include(:child_group_missing, :dry_run_summary)
61
+ expect(@db.models[:group].find(name: 'created')).to be_nil
62
+ expect(parent.children_dataset[name: 'created']).to be_nil
63
+ end
64
+
65
+ it 'dry-runs removing child groups without deleting orphaned descendants' do
66
+ parent = @db.models[:group].create(name: 'parent')
67
+ child = @db.models[:group].create(name: 'child')
68
+ grandchild = @db.models[:group].create(name: 'grandchild')
69
+ host = @db.models[:host].create(name: 'child-host')
70
+ child.add_host(host)
71
+ parent.add_child(child)
72
+ child.add_child(grandchild)
73
+
74
+ result = operation.remove_children(
75
+ parent_group: parent,
76
+ parent_name: 'parent',
77
+ child_names: ['child'],
78
+ delete_orphans: true,
79
+ dry_run: true
80
+ )
81
+
82
+ expect(result.warning_count).to eq(0)
83
+ expect(result.events.map(&:type)).to include(
84
+ :recursively_delete_orphaned_group,
85
+ :destroying_group,
86
+ :adding_automatic_group_to_host,
87
+ :dry_run_summary
88
+ )
89
+ expect(parent.children_dataset[name: 'child']).not_to be_nil
90
+ expect(@db.models[:group].find(name: 'child')).not_to be_nil
91
+ expect(@db.models[:group].find(name: 'grandchild')).not_to be_nil
92
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
93
+ end
94
+
95
+ it 'removes child groups and recursively deletes orphan groups when requested' do
96
+ parent = @db.models[:group].create(name: 'parent')
97
+ child = @db.models[:group].create(name: 'child')
98
+ grandchild = @db.models[:group].create(name: 'grandchild')
99
+ host = @db.models[:host].create(name: 'child-host')
100
+ child.add_host(host)
101
+ parent.add_child(child)
102
+ child.add_child(grandchild)
103
+
104
+ result = operation.remove_children(
105
+ parent_group: parent,
106
+ parent_name: 'parent',
107
+ child_names: %w[missing child],
108
+ delete_orphans: true
109
+ )
110
+
111
+ expect(result.warning_count).to eq(1)
112
+ expect(result.events.map(&:type)).to include(
113
+ :child_association_missing,
114
+ :recursively_delete_orphaned_group,
115
+ :destroying_group,
116
+ :adding_automatic_group_to_host
117
+ )
118
+ expect(@db.models[:group].find(name: 'child')).to be_nil
119
+ expect(@db.models[:group].find(name: 'grandchild')).to be_nil
120
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
121
+ end
122
+ end