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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
require 'fileutils'
|
|
3
5
|
require 'tmpdir'
|
|
@@ -5,17 +7,23 @@ require 'tmpdir'
|
|
|
5
7
|
RSpec.describe 'Moose::Inventory::DB' do
|
|
6
8
|
def with_db_config(db_config)
|
|
7
9
|
saved_db = @db.instance_variable_get(:@db)
|
|
10
|
+
saved_models = @db.instance_variable_get(:@models)
|
|
11
|
+
saved_exceptions = @db.instance_variable_get(:@exceptions)
|
|
8
12
|
saved_settings = @config._settings.dup
|
|
9
13
|
|
|
10
14
|
begin
|
|
11
|
-
@db.
|
|
15
|
+
@db.reset_runtime_state
|
|
16
|
+
@db.init_exceptions
|
|
12
17
|
@config._settings.clear
|
|
13
18
|
@config._settings[:config] = { db: db_config }
|
|
14
19
|
yield
|
|
15
20
|
ensure
|
|
16
21
|
current_db = @db.instance_variable_get(:@db)
|
|
17
22
|
current_db.disconnect if current_db.respond_to?(:disconnect)
|
|
23
|
+
@db.reset_runtime_state
|
|
18
24
|
@db.instance_variable_set(:@db, saved_db)
|
|
25
|
+
@db.instance_variable_set(:@models, saved_models)
|
|
26
|
+
@db.instance_variable_set(:@exceptions, saved_exceptions)
|
|
19
27
|
@config._settings.clear
|
|
20
28
|
@config._settings.merge!(saved_settings)
|
|
21
29
|
end
|
|
@@ -28,9 +36,9 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
28
36
|
before(:all) do
|
|
29
37
|
# Set up the configuration object
|
|
30
38
|
@mockarg_parts = {
|
|
31
|
-
config:
|
|
32
|
-
format:
|
|
33
|
-
env:
|
|
39
|
+
config: File.join(spec_root, 'config/config.yml'),
|
|
40
|
+
format: 'yaml',
|
|
41
|
+
env: 'test'
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
@mockargs = []
|
|
@@ -69,39 +77,235 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
it 'raises a Moose DB exception for unsupported adapters' do
|
|
72
|
-
|
|
73
|
-
saved_models = @db.instance_variable_get(:@models)
|
|
74
|
-
saved_exceptions = @db.instance_variable_get(:@exceptions)
|
|
75
|
-
saved_settings = @config._settings.dup
|
|
76
|
-
|
|
77
|
-
begin
|
|
78
|
-
@db.instance_variable_set(:@db, nil)
|
|
79
|
-
@db.instance_variable_set(:@models, nil)
|
|
80
|
-
@db.instance_variable_set(:@exceptions, nil)
|
|
81
|
-
@config._settings.clear
|
|
82
|
-
@config._settings[:config] = { db: { adapter: 'unsupported' } }
|
|
83
|
-
|
|
80
|
+
with_db_config(adapter: 'unsupported') do
|
|
84
81
|
expect { @db.init }.to raise_error(
|
|
85
82
|
Moose::Inventory::DB::MooseDBException,
|
|
86
83
|
/database adapter unsupported is not yet supported/
|
|
87
84
|
)
|
|
88
85
|
expect(@db.exceptions[:moose]).to eq(Moose::Inventory::DB::MooseDBException)
|
|
89
|
-
ensure
|
|
90
|
-
@db.instance_variable_set(:@db, saved_db)
|
|
91
|
-
@db.instance_variable_set(:@models, saved_models)
|
|
92
|
-
@db.instance_variable_set(:@exceptions, saved_exceptions)
|
|
93
|
-
@config._settings.clear
|
|
94
|
-
@config._settings.merge!(saved_settings)
|
|
95
86
|
end
|
|
96
87
|
end
|
|
97
88
|
end
|
|
98
89
|
|
|
90
|
+
describe 'existing database upgrade behavior' do
|
|
91
|
+
def with_sqlite_fixture(schema_version: nil, tables: [])
|
|
92
|
+
Dir.mktmpdir('moose-schema-fixture') do |dir|
|
|
93
|
+
dbfile = File.join(dir, 'inventory.sqlite3')
|
|
94
|
+
seed_sqlite_fixture(dbfile, schema_version: schema_version, tables: tables)
|
|
95
|
+
|
|
96
|
+
with_db_config(adapter: 'sqlite3', file: dbfile) do
|
|
97
|
+
yield dbfile
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def seed_sqlite_fixture(dbfile, schema_version:, tables:)
|
|
103
|
+
fixture = Sequel.sqlite(dbfile)
|
|
104
|
+
tables.each { |table| create_fixture_table(fixture, table) }
|
|
105
|
+
unless schema_version.nil?
|
|
106
|
+
create_fixture_table(fixture, :schema_info)
|
|
107
|
+
fixture[:schema_info].insert(version: schema_version)
|
|
108
|
+
end
|
|
109
|
+
ensure
|
|
110
|
+
fixture&.disconnect
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def create_fixture_table(db, table)
|
|
114
|
+
return if db.table_exists?(table)
|
|
115
|
+
|
|
116
|
+
@db::TABLE_DEFINITIONS.fetch(table).call(db)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def fixture_tables_through(version)
|
|
120
|
+
@db::SCHEMA_MIGRATIONS.values_at(*(1..version)).flatten
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def expect_current_schema_after_init
|
|
124
|
+
@db.init
|
|
125
|
+
expect(@db.schema_version).to eq(@db::SCHEMA_VERSION)
|
|
126
|
+
expect(@db.status[:tables].values).to all(eq(true))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'declares ordered migrations through the current schema version' do
|
|
130
|
+
expect(@db.migration_versions).to eq((1..@db::SCHEMA_VERSION).to_a)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'exposes schema definitions from the schema migration module' do
|
|
134
|
+
expect(@db::SCHEMA_VERSION).to eq(@db::SchemaMigrations::SCHEMA_VERSION)
|
|
135
|
+
expect(@db::TABLE_DEFINITIONS).to equal(@db::SchemaMigrations::TABLE_DEFINITIONS)
|
|
136
|
+
expect(@db::SCHEMA_MIGRATIONS).to equal(@db::SchemaMigrations::SCHEMA_MIGRATIONS)
|
|
137
|
+
expect(@db::INDEX_DEFINITIONS).to equal(@db::SchemaMigrations::INDEX_DEFINITIONS)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'applies schema migrations one version at a time' do
|
|
141
|
+
with_sqlite_fixture(schema_version: 1, tables: %i[hosts groups schema_info]) do
|
|
142
|
+
@db.init_sqlite3
|
|
143
|
+
|
|
144
|
+
expect(@db).to receive(:apply_schema_migration!).ordered.with(1).and_call_original
|
|
145
|
+
expect(@db).to receive(:apply_schema_migration!).ordered.with(2).and_call_original
|
|
146
|
+
expect(@db).to receive(:apply_schema_migration!).ordered.with(3).and_call_original
|
|
147
|
+
expect(@db).to receive(:apply_schema_migration!).ordered.with(4).and_call_original
|
|
148
|
+
|
|
149
|
+
@db.migrate_schema!
|
|
150
|
+
|
|
151
|
+
expect(@db.schema_version).to eq(@db::SCHEMA_VERSION)
|
|
152
|
+
expect(@db.status[:tables][:audit_events]).to eq(true)
|
|
153
|
+
expect(@db.status[:tables][:tags]).to eq(true)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'creates unique and lookup indexes in schema version 4' do
|
|
158
|
+
expected = {
|
|
159
|
+
hostvars: :idx_hostvars_host_id_name,
|
|
160
|
+
groupvars: :idx_groupvars_group_id_name,
|
|
161
|
+
groups_hosts: :idx_groups_hosts_host_id_group_id,
|
|
162
|
+
groups_groups: :idx_groups_groups_parent_id_child_id,
|
|
163
|
+
hosts_tags: :idx_hosts_tags_host_id_tag_id,
|
|
164
|
+
groups_tags: :idx_groups_tags_group_id_tag_id
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
expected.each do |table, index_name|
|
|
168
|
+
expect(@db.db.indexes(table)).to include(index_name)
|
|
169
|
+
expect(@db.db.indexes(table).fetch(index_name)[:unique]).to eq(true)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'enforces unique host variable names per host' do
|
|
174
|
+
@db.reset
|
|
175
|
+
host = @db.models[:host].create(name: 'indexed-host')
|
|
176
|
+
@db.db[:hostvars].insert(host_id: host.id, name: 'env', value: 'prod')
|
|
177
|
+
|
|
178
|
+
expect do
|
|
179
|
+
@db.db[:hostvars].insert(host_id: host.id, name: 'env', value: 'prod')
|
|
180
|
+
end.to raise_error(Sequel::UniqueConstraintViolation)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'enforces unique host-group associations' do
|
|
184
|
+
@db.reset
|
|
185
|
+
host = @db.models[:host].create(name: 'indexed-host')
|
|
186
|
+
group = @db.models[:group].create(name: 'indexed-group')
|
|
187
|
+
@db.db[:groups_hosts].insert(host_id: host.id, group_id: group.id)
|
|
188
|
+
|
|
189
|
+
expect do
|
|
190
|
+
@db.db[:groups_hosts].insert(host_id: host.id, group_id: group.id)
|
|
191
|
+
end.to raise_error(Sequel::UniqueConstraintViolation)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'removes duplicate rows with identical values before adding unique indexes' do
|
|
195
|
+
with_sqlite_fixture(schema_version: 3, tables: fixture_tables_through(3)) do
|
|
196
|
+
@db.init_sqlite3
|
|
197
|
+
host_id = @db.db[:hosts].insert(name: 'dup-host')
|
|
198
|
+
group_id = @db.db[:groups].insert(name: 'dup-group')
|
|
199
|
+
@db.db[:hostvars].insert(host_id: host_id, name: 'env', value: 'prod')
|
|
200
|
+
@db.db[:hostvars].insert(host_id: host_id, name: 'env', value: 'prod')
|
|
201
|
+
@db.db[:groups_hosts].insert(host_id: host_id, group_id: group_id)
|
|
202
|
+
@db.db[:groups_hosts].insert(host_id: host_id, group_id: group_id)
|
|
203
|
+
|
|
204
|
+
@db.migrate_schema!
|
|
205
|
+
|
|
206
|
+
expect(@db.schema_version).to eq(@db::SCHEMA_VERSION)
|
|
207
|
+
expect(@db.db[:hostvars].where(host_id: host_id, name: 'env').count).to eq(1)
|
|
208
|
+
expect(@db.db[:groups_hosts].where(host_id: host_id, group_id: group_id).count).to eq(1)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'refuses index migration when duplicate variable rows have conflicting values' do
|
|
213
|
+
with_sqlite_fixture(schema_version: 3, tables: fixture_tables_through(3)) do
|
|
214
|
+
@db.init_sqlite3
|
|
215
|
+
host_id = @db.db[:hosts].insert(name: 'conflict-host')
|
|
216
|
+
@db.db[:hostvars].insert(host_id: host_id, name: 'env', value: 'prod')
|
|
217
|
+
@db.db[:hostvars].insert(host_id: host_id, name: 'env', value: 'dev')
|
|
218
|
+
|
|
219
|
+
expect { @db.migrate_schema! }.to raise_error(
|
|
220
|
+
Moose::Inventory::DB::MooseDBException,
|
|
221
|
+
/conflicting duplicates/
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'upgrades a pre-schema-info sqlite database by creating missing additive tables' do
|
|
227
|
+
with_sqlite_fixture(tables: %i[hosts groups]) do
|
|
228
|
+
expect_current_schema_after_init
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'upgrades a schema version 1 sqlite database to the current schema' do
|
|
233
|
+
with_sqlite_fixture(schema_version: 1, tables: %i[hosts groups]) do
|
|
234
|
+
expect_current_schema_after_init
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'upgrades a schema version 2 sqlite database to the current schema' do
|
|
239
|
+
with_sqlite_fixture(schema_version: 2, tables: %i[hosts groups schema_info]) do
|
|
240
|
+
expect_current_schema_after_init
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'opens a current sqlite database without changing its schema version' do
|
|
245
|
+
with_sqlite_fixture(schema_version: @db::SCHEMA_VERSION, tables: %i[hosts groups schema_info tags]) do
|
|
246
|
+
expect_current_schema_after_init
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'refuses to open a database from a future schema version' do
|
|
251
|
+
with_sqlite_fixture(schema_version: @db::SCHEMA_VERSION + 1, tables: %i[hosts groups schema_info]) do
|
|
252
|
+
expect { @db.init }.to raise_error(
|
|
253
|
+
Moose::Inventory::DB::MooseDBException,
|
|
254
|
+
/newer than supported version #{@db::SCHEMA_VERSION}/
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'refuses to migrate a database from a future schema version' do
|
|
260
|
+
with_sqlite_fixture(schema_version: @db::SCHEMA_VERSION + 1, tables: %i[hosts groups schema_info]) do
|
|
261
|
+
@db.init_sqlite3
|
|
262
|
+
|
|
263
|
+
expect { @db.migrate! }.to raise_error(
|
|
264
|
+
Moose::Inventory::DB::MooseDBException,
|
|
265
|
+
/newer than supported version #{@db::SCHEMA_VERSION}/
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'repairs dirty partial schemas during startup without changing the final schema version' do
|
|
271
|
+
with_sqlite_fixture(schema_version: @db::SCHEMA_VERSION, tables: %i[hosts groups schema_info]) do
|
|
272
|
+
@db.init
|
|
273
|
+
|
|
274
|
+
status = @db.status
|
|
275
|
+
|
|
276
|
+
expect(status[:schema_version]).to eq(@db::SCHEMA_VERSION)
|
|
277
|
+
expect(status[:tables].values).to all(eq(true))
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
99
281
|
describe '.init_exceptions()' do
|
|
100
282
|
it 'is responsive' do
|
|
101
283
|
expect(@db.respond_to?(:init_exceptions)).to eq(true)
|
|
102
284
|
end
|
|
103
285
|
end
|
|
104
286
|
|
|
287
|
+
describe '.reset_runtime_state()' do
|
|
288
|
+
it 'clears cached db, models, and exceptions' do
|
|
289
|
+
saved_db = @db.instance_variable_get(:@db)
|
|
290
|
+
saved_models = @db.instance_variable_get(:@models)
|
|
291
|
+
saved_exceptions = @db.instance_variable_get(:@exceptions)
|
|
292
|
+
|
|
293
|
+
@db.instance_variable_set(:@db, :fake_db)
|
|
294
|
+
@db.instance_variable_set(:@models, { fake: true })
|
|
295
|
+
@db.instance_variable_set(:@exceptions, { fake: true })
|
|
296
|
+
|
|
297
|
+
@db.reset_runtime_state
|
|
298
|
+
|
|
299
|
+
expect(@db.db).to be_nil
|
|
300
|
+
expect(@db.models).to be_nil
|
|
301
|
+
expect(@db.exceptions).to be_nil
|
|
302
|
+
|
|
303
|
+
@db.instance_variable_set(:@db, saved_db)
|
|
304
|
+
@db.instance_variable_set(:@models, saved_models)
|
|
305
|
+
@db.instance_variable_set(:@exceptions, saved_exceptions)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
105
309
|
describe '.connect()' do
|
|
106
310
|
it 'dispatches the documented sqlite3 adapter to the sqlite initializer' do
|
|
107
311
|
with_db_config(adapter: 'sqlite3') do
|
|
@@ -146,18 +350,20 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
146
350
|
|
|
147
351
|
it 'creates nested parent directories for configured database files' do
|
|
148
352
|
saved_db = @db.instance_variable_get(:@db)
|
|
353
|
+
saved_models = @db.instance_variable_get(:@models)
|
|
354
|
+
saved_exceptions = @db.instance_variable_get(:@exceptions)
|
|
149
355
|
saved_settings = @config._settings.dup
|
|
150
356
|
tmpdir = Dir.mktmpdir('moose-inventory-sqlite')
|
|
151
357
|
nested_dbfile = File.join(tmpdir, 'one', 'two', 'inventory.db')
|
|
152
358
|
|
|
153
359
|
begin
|
|
154
|
-
@db.
|
|
360
|
+
@db.reset_runtime_state
|
|
155
361
|
@config._settings.clear
|
|
156
362
|
@config._settings[:config] = {
|
|
157
363
|
db: {
|
|
158
364
|
adapter: 'sqlite3',
|
|
159
|
-
file: nested_dbfile
|
|
160
|
-
}
|
|
365
|
+
file: nested_dbfile
|
|
366
|
+
}
|
|
161
367
|
}
|
|
162
368
|
|
|
163
369
|
@db.init_sqlite3
|
|
@@ -167,7 +373,10 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
167
373
|
ensure
|
|
168
374
|
current_db = @db.instance_variable_get(:@db)
|
|
169
375
|
current_db.disconnect if current_db.respond_to?(:disconnect)
|
|
376
|
+
@db.reset_runtime_state
|
|
170
377
|
@db.instance_variable_set(:@db, saved_db)
|
|
378
|
+
@db.instance_variable_set(:@models, saved_models)
|
|
379
|
+
@db.instance_variable_set(:@exceptions, saved_exceptions)
|
|
171
380
|
@config._settings.clear
|
|
172
381
|
@config._settings.merge!(saved_settings)
|
|
173
382
|
FileUtils.remove_entry(tmpdir) if tmpdir && Dir.exist?(tmpdir)
|
|
@@ -207,13 +416,13 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
207
416
|
it 'uses a mysql password from the configured environment variable' do
|
|
208
417
|
saved_db = @db.instance_variable_get(:@db)
|
|
209
418
|
saved_settings = @config._settings.dup
|
|
210
|
-
saved_password = ENV
|
|
419
|
+
saved_password = ENV.fetch('MOOSE_INVENTORY_MYSQL_PASSWORD', nil)
|
|
211
420
|
mysql_config = {
|
|
212
421
|
adapter: 'mysql',
|
|
213
422
|
host: 'localhost',
|
|
214
423
|
database: 'moose_inventory_test',
|
|
215
424
|
user: 'moose',
|
|
216
|
-
password_env: 'MOOSE_INVENTORY_MYSQL_PASSWORD'
|
|
425
|
+
password_env: 'MOOSE_INVENTORY_MYSQL_PASSWORD'
|
|
217
426
|
}
|
|
218
427
|
|
|
219
428
|
begin
|
|
@@ -251,7 +460,7 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
251
460
|
host: 'localhost',
|
|
252
461
|
database: 'moose_inventory_test',
|
|
253
462
|
user: 'moose',
|
|
254
|
-
password: 'secret'
|
|
463
|
+
password: 'secret'
|
|
255
464
|
}
|
|
256
465
|
|
|
257
466
|
begin
|
|
@@ -292,7 +501,7 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
292
501
|
end
|
|
293
502
|
|
|
294
503
|
it 'raises a Moose DB exception when password_env points to an unset variable' do
|
|
295
|
-
saved_password = ENV
|
|
504
|
+
saved_password = ENV.fetch('MOOSE_INVENTORY_POSTGRES_PASSWORD', nil)
|
|
296
505
|
ENV.delete('MOOSE_INVENTORY_POSTGRES_PASSWORD')
|
|
297
506
|
|
|
298
507
|
begin
|
|
@@ -316,13 +525,13 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
316
525
|
it 'uses a postgresql password from the configured environment variable' do
|
|
317
526
|
saved_db = @db.instance_variable_get(:@db)
|
|
318
527
|
saved_settings = @config._settings.dup
|
|
319
|
-
saved_password = ENV
|
|
528
|
+
saved_password = ENV.fetch('MOOSE_INVENTORY_POSTGRES_PASSWORD', nil)
|
|
320
529
|
postgresql_config = {
|
|
321
530
|
adapter: 'postgresql',
|
|
322
531
|
host: 'localhost',
|
|
323
532
|
database: 'moose_inventory_test',
|
|
324
533
|
user: 'moose',
|
|
325
|
-
password_env: 'MOOSE_INVENTORY_POSTGRES_PASSWORD'
|
|
534
|
+
password_env: 'MOOSE_INVENTORY_POSTGRES_PASSWORD'
|
|
326
535
|
}
|
|
327
536
|
|
|
328
537
|
begin
|
|
@@ -360,7 +569,7 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
360
569
|
host: 'localhost',
|
|
361
570
|
database: 'moose_inventory_test',
|
|
362
571
|
user: 'moose',
|
|
363
|
-
password: 'secret'
|
|
572
|
+
password: 'secret'
|
|
364
573
|
}
|
|
365
574
|
|
|
366
575
|
begin
|
|
@@ -441,6 +650,70 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
441
650
|
end
|
|
442
651
|
end
|
|
443
652
|
|
|
653
|
+
describe '.busy_retry_delay()' do
|
|
654
|
+
it 'uses deterministic capped exponential backoff delays' do
|
|
655
|
+
expect(@db.busy_retry_delay(1)).to eq(0.05)
|
|
656
|
+
expect(@db.busy_retry_delay(2)).to eq(0.1)
|
|
657
|
+
expect(@db.busy_retry_delay(5)).to eq(0.8)
|
|
658
|
+
expect(@db.busy_retry_delay(6)).to eq(1.0)
|
|
659
|
+
expect(@db.busy_retry_delay(10)).to eq(1.0)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
describe '.busy_database_error?()' do
|
|
664
|
+
it 'identifies Sequel busy database errors by message' do
|
|
665
|
+
expect(@db.busy_database_error?(Sequel::DatabaseError.new('BusyException: locked'))).to eq(true)
|
|
666
|
+
expect(@db.busy_database_error?(Sequel::DatabaseError.new('other database failure'))).to eq(false)
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
describe '.retry_busy_transaction()' do
|
|
671
|
+
it 'sleeps for the deterministic retry delay before another busy retry' do
|
|
672
|
+
delays = []
|
|
673
|
+
error = Sequel::DatabaseError.new('BusyException: database is locked')
|
|
674
|
+
|
|
675
|
+
@db.retry_busy_transaction(error, 3, sleeper: ->(delay) { delays << delay })
|
|
676
|
+
|
|
677
|
+
expect(delays).to eq([0.2])
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
it 'raises the original error when the retry limit is exceeded' do
|
|
681
|
+
error = Sequel::DatabaseError.new('BusyException: database is locked')
|
|
682
|
+
|
|
683
|
+
actual_stderr = capture(:STDERR) do
|
|
684
|
+
expect { @db.retry_busy_transaction(error, 11, sleeper: ->(_delay) {}) }
|
|
685
|
+
.to raise_error(error)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
expect(actual_stderr).to include('The database appears to be locked by another process')
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
describe '.purge()' do
|
|
693
|
+
it 'uses drop_table for non-sqlite adapters' do
|
|
694
|
+
saved_db = @db.instance_variable_get(:@db)
|
|
695
|
+
|
|
696
|
+
begin
|
|
697
|
+
fake_db = instance_double('DB')
|
|
698
|
+
@db.instance_variable_set(:@db, fake_db)
|
|
699
|
+
allow(@db).to receive(:sqlite_adapter?).and_return(false)
|
|
700
|
+
expect(fake_db).to receive(:drop_table).with(
|
|
701
|
+
:hosts,
|
|
702
|
+
:hostvars,
|
|
703
|
+
:groups,
|
|
704
|
+
:groupvars,
|
|
705
|
+
:group_hosts,
|
|
706
|
+
if_exists: true,
|
|
707
|
+
cascade: true
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
@db.purge
|
|
711
|
+
ensure
|
|
712
|
+
@db.instance_variable_set(:@db, saved_db)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
444
717
|
describe '.reset()' do
|
|
445
718
|
it 'should be responsive' do
|
|
446
719
|
result = @db.respond_to?(:reset)
|
|
@@ -455,7 +728,6 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
455
728
|
# Reset the DB
|
|
456
729
|
@db.reset
|
|
457
730
|
|
|
458
|
-
#
|
|
459
731
|
hosts = @db.models[:host].all
|
|
460
732
|
expect(hosts.count).to eq(0)
|
|
461
733
|
|
|
@@ -474,6 +746,57 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
474
746
|
expect(result).to eq(true)
|
|
475
747
|
end
|
|
476
748
|
|
|
749
|
+
it 'retries busy database transactions before succeeding' do
|
|
750
|
+
saved_db = @db.instance_variable_get(:@db)
|
|
751
|
+
fake_db = Class.new do
|
|
752
|
+
attr_reader :attempts
|
|
753
|
+
|
|
754
|
+
def initialize(error)
|
|
755
|
+
@error = error
|
|
756
|
+
@attempts = 0
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def transaction(savepoint:)
|
|
760
|
+
raise ArgumentError, 'expected savepoint' unless savepoint
|
|
761
|
+
|
|
762
|
+
@attempts += 1
|
|
763
|
+
raise @error if @attempts == 1
|
|
764
|
+
|
|
765
|
+
yield
|
|
766
|
+
end
|
|
767
|
+
end.new(Sequel::DatabaseError.new('BusyException: database is locked'))
|
|
768
|
+
|
|
769
|
+
begin
|
|
770
|
+
@db.instance_variable_set(:@db, fake_db)
|
|
771
|
+
allow(@db).to receive(:retry_busy_transaction)
|
|
772
|
+
|
|
773
|
+
result = @db.transaction { :ok }
|
|
774
|
+
|
|
775
|
+
expect(result).to eq(:ok)
|
|
776
|
+
expect(fake_db.attempts).to eq(2)
|
|
777
|
+
expect(@db).to have_received(:retry_busy_transaction).once
|
|
778
|
+
ensure
|
|
779
|
+
@db.instance_variable_set(:@db, saved_db)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
it 're-raises non-busy database errors without retrying' do
|
|
784
|
+
saved_db = @db.instance_variable_get(:@db)
|
|
785
|
+
error = Sequel::DatabaseError.new('constraint failed')
|
|
786
|
+
fake_db = instance_double('DB')
|
|
787
|
+
|
|
788
|
+
begin
|
|
789
|
+
@db.instance_variable_set(:@db, fake_db)
|
|
790
|
+
allow(fake_db).to receive(:transaction).and_raise(error)
|
|
791
|
+
allow(@db).to receive(:retry_busy_transaction)
|
|
792
|
+
|
|
793
|
+
expect { @db.transaction { :ignored } }.to raise_error(error)
|
|
794
|
+
expect(@db).not_to have_received(:retry_busy_transaction)
|
|
795
|
+
ensure
|
|
796
|
+
@db.instance_variable_set(:@db, saved_db)
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
477
800
|
it 'should perform transactions' do
|
|
478
801
|
hosts = @db.models[:host].all
|
|
479
802
|
count = { initial: hosts.count, items: 0 }
|
|
@@ -505,7 +828,7 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
505
828
|
count[:items] = count[:items] + 1
|
|
506
829
|
@db.models[:host].create(name: "rollback-#{count[:items]}")
|
|
507
830
|
end
|
|
508
|
-
|
|
831
|
+
raise Sequel::Rollback, 'Test error'
|
|
509
832
|
end
|
|
510
833
|
|
|
511
834
|
hosts = @db.models[:host].all
|
|
@@ -524,7 +847,7 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
524
847
|
begin
|
|
525
848
|
actual = runner do
|
|
526
849
|
@db.transaction do
|
|
527
|
-
|
|
850
|
+
raise @db.exceptions[:moose], 'Trace regression target'
|
|
528
851
|
end
|
|
529
852
|
end
|
|
530
853
|
ensure
|
|
@@ -546,7 +869,7 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
546
869
|
begin
|
|
547
870
|
actual = runner do
|
|
548
871
|
@db.transaction do
|
|
549
|
-
|
|
872
|
+
raise @db.exceptions[:moose], 'Trace regression target'
|
|
550
873
|
end
|
|
551
874
|
end
|
|
552
875
|
ensure
|
|
@@ -564,5 +887,42 @@ RSpec.describe 'Moose::Inventory::DB' do
|
|
|
564
887
|
expect(actual[:STDERR]).to include("ERROR: Trace regression target\n")
|
|
565
888
|
expect(actual[:STDERR]).not_to include('NoMethodError')
|
|
566
889
|
end
|
|
890
|
+
|
|
891
|
+
it 'warns and re-raises ordinary StandardError failures' do
|
|
892
|
+
actual = runner do
|
|
893
|
+
@db.transaction do
|
|
894
|
+
raise 'generic failure'
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
expect(actual[:aborted]).to eq(false)
|
|
899
|
+
expect(actual[:unexpected].class).to eq(RuntimeError)
|
|
900
|
+
expect(actual[:unexpected].message).to eq('generic failure')
|
|
901
|
+
expect(actual[:STDERR]).to eq(
|
|
902
|
+
"An error occurred during a transaction, any changes have been rolled back.\n"
|
|
903
|
+
)
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
it 'warns and re-raises SystemExit failures triggered inside the transaction' do
|
|
907
|
+
actual = runner do
|
|
908
|
+
@db.transaction do
|
|
909
|
+
raise SystemExit.new(7), 'stop now'
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
expect(actual[:unexpected]).to eq(false)
|
|
914
|
+
expect(actual[:aborted]).to eq(true)
|
|
915
|
+
expect(actual[:STDERR]).to eq(
|
|
916
|
+
"An error occurred during a transaction, any changes have been rolled back.\n"
|
|
917
|
+
)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
it 'does not swallow non-rescued fatal exceptions such as interrupts' do
|
|
921
|
+
expect do
|
|
922
|
+
@db.transaction do
|
|
923
|
+
raise Interrupt, 'ctrl-c'
|
|
924
|
+
end
|
|
925
|
+
end.to raise_error(Interrupt, 'ctrl-c')
|
|
926
|
+
end
|
|
567
927
|
end
|
|
568
928
|
end
|
|
@@ -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:
|
|
12
|
-
format:
|
|
13
|
-
env:
|
|
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
|