浏览代码

Include bootstrapped configuration files in spot check (#1143).

Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1143
Dan Helfman 1 周之前
父节点
当前提交
82e962eae2
共有 5 个文件被更改,包括 219 次插入276 次删除
  1. 3 1
      NEWS
  2. 32 11
      borgmatic/actions/check.py
  3. 58 53
      borgmatic/actions/config/bootstrap.py
  4. 93 203
      tests/unit/actions/config/test_bootstrap.py
  5. 33 8
      tests/unit/actions/test_check.py

+ 3 - 1
NEWS

@@ -4,7 +4,9 @@
    use.
    use.
  * #1126: Create LVM snapshots as read-write to avoid an error when snapshotting ext4 filesystems
  * #1126: Create LVM snapshots as read-write to avoid an error when snapshotting ext4 filesystems
    with orphaned files that need recovery.
    with orphaned files that need recovery.
- * #1139: When making HTTP requests in monitoring hooks, set "borgmatic" as the user agent.
+ * #1133: Fix the "spot" check to include borgmatic configuration files that were backed up to
+   support the "bootstrap" action.
+ * #1139: Set "borgmatic" as the user agent when connecting to monitoring services.
  * When running tests, use Ruff for faster and more comprehensive code linting and formatting,
  * When running tests, use Ruff for faster and more comprehensive code linting and formatting,
    replacing Flake8, Black, isort, etc.
    replacing Flake8, Black, isort, etc.
  * Switch from pipx to uv for installing development tools, and added tox-uv for speeding up test
  * Switch from pipx to uv for installing development tools, and added tox-uv for speeding up test

+ 32 - 11
borgmatic/actions/check.py

@@ -11,12 +11,14 @@ import shlex
 import shutil
 import shutil
 import textwrap
 import textwrap
 
 
+import borgmatic.actions.config.bootstrap
 import borgmatic.actions.pattern
 import borgmatic.actions.pattern
 import borgmatic.borg.check
 import borgmatic.borg.check
 import borgmatic.borg.create
 import borgmatic.borg.create
 import borgmatic.borg.environment
 import borgmatic.borg.environment
 import borgmatic.borg.extract
 import borgmatic.borg.extract
 import borgmatic.borg.list
 import borgmatic.borg.list
+import borgmatic.borg.pattern
 import borgmatic.borg.repo_list
 import borgmatic.borg.repo_list
 import borgmatic.borg.state
 import borgmatic.borg.state
 import borgmatic.config.paths
 import borgmatic.config.paths
@@ -358,11 +360,15 @@ def collect_spot_check_source_paths(
     local_path,
     local_path,
     remote_path,
     remote_path,
     borgmatic_runtime_directory,
     borgmatic_runtime_directory,
+    bootstrap_config_paths,
 ):
 ):
     '''
     '''
     Given a repository configuration dict, a configuration dict, the local Borg version, global
     Given a repository configuration dict, a configuration dict, the local Borg version, global
-    arguments as an argparse.Namespace instance, the local Borg path, and the remote Borg path,
-    collect the source paths that Borg would use in an actual create (but only include files).
+    arguments as an argparse.Namespace instance, the local Borg path, the remote Borg path, and the
+    bootstrap configuration paths as read from an archive's manifest, collect the source paths that
+    Borg would use in an actual create (but only include files). As part of this, include the
+    bootstrap configuration paths, so that any configuration files included in the archive to
+    support bootstrapping are also spot checked.
     '''
     '''
     stream_processes = any(
     stream_processes = any(
         borgmatic.hooks.dispatch.call_hooks(
         borgmatic.hooks.dispatch.call_hooks(
@@ -382,7 +388,14 @@ def collect_spot_check_source_paths(
             list_details=True,
             list_details=True,
         ),
         ),
         patterns=borgmatic.actions.pattern.process_patterns(
         patterns=borgmatic.actions.pattern.process_patterns(
-            borgmatic.actions.pattern.collect_patterns(config),
+            borgmatic.actions.pattern.collect_patterns(config)
+            + tuple(
+                borgmatic.borg.pattern.Pattern(
+                    config_path,
+                    source=borgmatic.borg.pattern.Pattern_source.INTERNAL,
+                )
+                for config_path in bootstrap_config_paths
+            ),
             config,
             config,
             working_directory,
             working_directory,
         ),
         ),
@@ -609,27 +622,35 @@ def spot_check(
             'The data_tolerance_percentage must be less than or equal to the data_sample_percentage',
             'The data_tolerance_percentage must be less than or equal to the data_sample_percentage',
         )
         )
 
 
-    source_paths = collect_spot_check_source_paths(
-        repository,
+    archive = borgmatic.borg.repo_list.resolve_archive_name(
+        repository['path'],
+        'latest',
         config,
         config,
         local_borg_version,
         local_borg_version,
         global_arguments,
         global_arguments,
         local_path,
         local_path,
         remote_path,
         remote_path,
-        borgmatic_runtime_directory,
     )
     )
-    logger.debug(f'{len(source_paths)} total source paths for spot check')
+    logger.debug(f'Using archive {archive} for spot check')
 
 
-    archive = borgmatic.borg.repo_list.resolve_archive_name(
-        repository['path'],
-        'latest',
+    source_paths = collect_spot_check_source_paths(
+        repository,
         config,
         config,
         local_borg_version,
         local_borg_version,
         global_arguments,
         global_arguments,
         local_path,
         local_path,
         remote_path,
         remote_path,
+        borgmatic_runtime_directory,
+        bootstrap_config_paths=borgmatic.actions.config.bootstrap.load_config_paths_from_archive(
+            repository['path'],
+            archive,
+            config,
+            local_borg_version,
+            global_arguments,
+            borgmatic_runtime_directory,
+        ),
     )
     )
-    logger.debug(f'Using archive {archive} for spot check')
+    logger.debug(f'{len(source_paths)} total source paths for spot check')
 
 
     archive_paths = collect_spot_check_archive_paths(
     archive_paths = collect_spot_check_archive_paths(
         repository,
         repository,

+ 58 - 53
borgmatic/actions/config/bootstrap.py

@@ -16,67 +16,68 @@ def make_bootstrap_config(bootstrap_arguments):
     Given the bootstrap arguments as an argparse.Namespace, return a corresponding config dict.
     Given the bootstrap arguments as an argparse.Namespace, return a corresponding config dict.
     '''
     '''
     return {
     return {
-        'ssh_command': bootstrap_arguments.ssh_command,
+        'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory,
+        'local_path': bootstrap_arguments.local_path,
+        'remote_path': bootstrap_arguments.remote_path,
         # In case the repo has been moved or is accessed from a different path at the point of
         # In case the repo has been moved or is accessed from a different path at the point of
         # bootstrapping.
         # bootstrapping.
         'relocated_repo_access_is_ok': True,
         'relocated_repo_access_is_ok': True,
+        'ssh_command': bootstrap_arguments.ssh_command,
+        'user_runtime_directory': bootstrap_arguments.user_runtime_directory,
     }
     }
 
 
 
 
-def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_borg_version):
+def load_config_paths_from_archive(
+    repository_path,
+    archive_name,
+    config,
+    local_borg_version,
+    global_arguments,
+    borgmatic_runtime_directory,
+):
     '''
     '''
-    Given an archive name, the bootstrap arguments as an argparse.Namespace (containing the
-    repository and archive name, Borg local path, Borg remote path, borgmatic runtime directory,
-    borgmatic source directory, destination directory, and whether to strip components), the global
-    arguments as an argparse.Namespace (containing the dry run flag and the local borg version),
-    return the config paths from the manifest.json file in the borgmatic source directory or runtime
-    directory after extracting it from the repository archive.
+    Given a repository path, an archive name, a configuration dict, the local Borg version, the
+    global arguments as an argparse.Namespace, and the borgmatic runtime directory, return the
+    config paths from the manifest.json file in the borgmatic source directory or runtime directory
+    within the repository archive.
 
 
     Raise ValueError if the manifest JSON is missing, can't be decoded, or doesn't contain the
     Raise ValueError if the manifest JSON is missing, can't be decoded, or doesn't contain the
     expected configuration path data.
     expected configuration path data.
     '''
     '''
-    borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(
-        {'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory},
-    )
-    config = make_bootstrap_config(bootstrap_arguments)
-
     # Probe for the manifest file in multiple locations, as the default location has moved to the
     # Probe for the manifest file in multiple locations, as the default location has moved to the
     # borgmatic runtime directory (which gets stored as just "/borgmatic" with Borg 1.4+). But we
     # borgmatic runtime directory (which gets stored as just "/borgmatic" with Borg 1.4+). But we
     # still want to support reading the manifest from previously created archives as well.
     # still want to support reading the manifest from previously created archives as well.
-    with borgmatic.config.paths.Runtime_directory(
-        {'user_runtime_directory': bootstrap_arguments.user_runtime_directory},
-    ) as borgmatic_runtime_directory:
-        for base_directory in (
-            'borgmatic',
-            borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
-            borgmatic_source_directory,
-        ):
-            borgmatic_manifest_path = 'sh:' + os.path.join(
-                base_directory,
-                'bootstrap',
-                'manifest.json',
-            )
-
-            extract_process = borgmatic.borg.extract.extract_archive(
-                global_arguments.dry_run,
-                bootstrap_arguments.repository,
-                archive_name,
-                [borgmatic_manifest_path],
-                config,
-                local_borg_version,
-                global_arguments,
-                local_path=bootstrap_arguments.local_path,
-                remote_path=bootstrap_arguments.remote_path,
-                extract_to_stdout=True,
-            )
-            manifest_json = extract_process.stdout.read()
-
-            if manifest_json:
-                break
-        else:
-            raise ValueError(
-                'Cannot read configuration paths from archive due to missing bootstrap manifest',
-            )
+    for base_directory in (
+        'borgmatic',
+        borgmatic.config.paths.make_runtime_directory_glob(borgmatic_runtime_directory),
+        borgmatic.config.paths.get_borgmatic_source_directory(config),
+    ):
+        borgmatic_manifest_path = 'sh:' + os.path.join(
+            base_directory,
+            'bootstrap',
+            'manifest.json',
+        )
+
+        extract_process = borgmatic.borg.extract.extract_archive(
+            global_arguments.dry_run,
+            repository_path,
+            archive_name,
+            [borgmatic_manifest_path],
+            config,
+            local_borg_version,
+            global_arguments,
+            local_path=config.get('local_path'),
+            remote_path=config.get('remote_path'),
+            extract_to_stdout=True,
+        )
+        manifest_json = extract_process.stdout.read()
+
+        if manifest_json:
+            break
+    else:
+        raise ValueError(
+            'Cannot read configuration paths from archive due to missing archive or bootstrap manifest',
+        )
 
 
     try:
     try:
         manifest_data = json.loads(manifest_json)
         manifest_data = json.loads(manifest_json)
@@ -110,12 +111,16 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
         local_path=bootstrap_arguments.local_path,
         local_path=bootstrap_arguments.local_path,
         remote_path=bootstrap_arguments.remote_path,
         remote_path=bootstrap_arguments.remote_path,
     )
     )
-    manifest_config_paths = get_config_paths(
-        archive_name,
-        bootstrap_arguments,
-        global_arguments,
-        local_borg_version,
-    )
+
+    with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
+        manifest_config_paths = load_config_paths_from_archive(
+            bootstrap_arguments.repository,
+            archive_name,
+            config,
+            local_borg_version,
+            global_arguments,
+            borgmatic_runtime_directory,
+        )
 
 
     logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
     logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
 
 

+ 93 - 203
tests/unit/actions/config/test_bootstrap.py

@@ -4,38 +4,32 @@ from flexmock import flexmock
 from borgmatic.actions.config import bootstrap as module
 from borgmatic.actions.config import bootstrap as module
 
 
 
 
-def test_make_bootstrap_config_uses_ssh_command_argument():
-    ssh_command = flexmock()
+def test_make_bootstrap_config_uses_bootstrap_arguments():
+    config = module.make_bootstrap_config(
+        flexmock(
+            borgmatic_source_directory='/source',
+            local_path='borg1',
+            remote_path='borg2',
+            ssh_command='ssh',
+            user_runtime_directory='/run',
+        )
+    )
 
 
-    config = module.make_bootstrap_config(flexmock(ssh_command=ssh_command))
-    assert config['ssh_command'] == ssh_command
+    assert config['borgmatic_source_directory'] == '/source'
+    assert config['local_path'] == 'borg1'
+    assert config['remote_path'] == 'borg2'
     assert config['relocated_repo_access_is_ok']
     assert config['relocated_repo_access_is_ok']
+    assert config['ssh_command'] == 'ssh'
+    assert config['user_runtime_directory'] == '/run'
 
 
 
 
-def test_get_config_paths_returns_list_of_config_paths():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_source_directory',
-    ).and_return('/source')
-    flexmock(module).should_receive('make_bootstrap_config').and_return({})
-    bootstrap_arguments = flexmock(
-        repository='repo',
-        archive='archive',
-        ssh_command=None,
-        local_path='borg7',
-        remote_path='borg8',
-        borgmatic_source_directory=None,
-        user_runtime_directory=None,
-    )
-    global_arguments = flexmock(
-        dry_run=False,
-    )
-    local_borg_version = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        flexmock(),
-    )
+def test_load_config_paths_from_archive_returns_list_of_config_paths():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'make_runtime_directory_glob',
         'make_runtime_directory_glob',
     ).replace_with(lambda path: path)
     ).replace_with(lambda path: path)
+    flexmock(module.borgmatic.config.paths).should_receive(
+        'get_borgmatic_source_directory',
+    ).and_return('/source')
     extract_process = flexmock(
     extract_process = flexmock(
         stdout=flexmock(
         stdout=flexmock(
             read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
             read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
@@ -45,54 +39,38 @@ def test_get_config_paths_returns_list_of_config_paths():
         extract_process,
         extract_process,
     )
     )
 
 
-    assert module.get_config_paths(
+    assert module.load_config_paths_from_archive(
+        'repo',
         'archive',
         'archive',
-        bootstrap_arguments,
-        global_arguments,
-        local_borg_version,
+        config={},
+        local_borg_version=flexmock(),
+        global_arguments=flexmock(dry_run=False),
+        borgmatic_runtime_directory='/run',
     ) == ['/borgmatic/config.yaml']
     ) == ['/borgmatic/config.yaml']
 
 
 
 
-def test_get_config_paths_probes_for_manifest():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_source_directory',
-    ).and_return('/source')
-    flexmock(module).should_receive('make_bootstrap_config').and_return({})
-    bootstrap_arguments = flexmock(
-        repository='repo',
-        archive='archive',
-        ssh_command=None,
-        local_path='borg7',
-        remote_path='borg8',
-        borgmatic_source_directory=None,
-        user_runtime_directory=None,
-    )
-    global_arguments = flexmock(
-        dry_run=False,
-    )
-    local_borg_version = flexmock()
-    borgmatic_runtime_directory = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        borgmatic_runtime_directory,
-    )
+def test_load_config_paths_from_archive_probes_for_manifest():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'make_runtime_directory_glob',
         'make_runtime_directory_glob',
     ).replace_with(lambda path: path)
     ).replace_with(lambda path: path)
-    flexmock(module.os.path).should_receive('join').with_args(
+    flexmock(module.borgmatic.config.paths).should_receive(
+        'get_borgmatic_source_directory',
+    ).and_return('/source')
+    flexmock(module.os.path).should_call('join').with_args(
         'borgmatic',
         'borgmatic',
         'bootstrap',
         'bootstrap',
         'manifest.json',
         'manifest.json',
-    ).and_return('borgmatic/bootstrap/manifest.json').once()
-    flexmock(module.os.path).should_receive('join').with_args(
-        borgmatic_runtime_directory,
+    ).once()
+    flexmock(module.os.path).should_call('join').with_args(
+        '/run',
         'bootstrap',
         'bootstrap',
         'manifest.json',
         'manifest.json',
-    ).and_return('run/borgmatic/bootstrap/manifest.json').once()
-    flexmock(module.os.path).should_receive('join').with_args(
+    ).once()
+    flexmock(module.os.path).should_call('join').with_args(
         '/source',
         '/source',
         'bootstrap',
         'bootstrap',
         'manifest.json',
         'manifest.json',
-    ).and_return('/source/bootstrap/manifest.json').once()
+    ).once()
     manifest_missing_extract_process = flexmock(
     manifest_missing_extract_process = flexmock(
         stdout=flexmock(read=lambda: None),
         stdout=flexmock(read=lambda: None),
     )
     )
@@ -105,128 +83,46 @@ def test_get_config_paths_probes_for_manifest():
         manifest_missing_extract_process,
         manifest_missing_extract_process,
     ).and_return(manifest_missing_extract_process).and_return(manifest_found_extract_process)
     ).and_return(manifest_missing_extract_process).and_return(manifest_found_extract_process)
 
 
-    assert module.get_config_paths(
+    assert module.load_config_paths_from_archive(
+        'repo',
         'archive',
         'archive',
-        bootstrap_arguments,
-        global_arguments,
-        local_borg_version,
+        config={},
+        local_borg_version=flexmock(),
+        global_arguments=flexmock(dry_run=False),
+        borgmatic_runtime_directory='/run',
     ) == ['/borgmatic/config.yaml']
     ) == ['/borgmatic/config.yaml']
 
 
 
 
-def test_get_config_paths_translates_ssh_command_argument_to_config():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_source_directory',
-    ).and_return('/source')
-    config = {}
-    flexmock(module).should_receive('make_bootstrap_config').and_return(config)
-    bootstrap_arguments = flexmock(
-        repository='repo',
-        archive='archive',
-        ssh_command='ssh -i key',
-        local_path='borg7',
-        remote_path='borg8',
-        borgmatic_source_directory=None,
-        user_runtime_directory=None,
-    )
-    global_arguments = flexmock(
-        dry_run=False,
-    )
-    local_borg_version = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        flexmock(),
-    )
+def test_load_config_paths_from_archive_with_missing_manifest_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'make_runtime_directory_glob',
         'make_runtime_directory_glob',
     ).replace_with(lambda path: path)
     ).replace_with(lambda path: path)
-    extract_process = flexmock(
-        stdout=flexmock(
-            read=lambda: '{"config_paths": ["/borgmatic/config.yaml"]}',
-        ),
-    )
-    flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
-        False,
-        'repo',
-        'archive',
-        object,
-        config,
-        object,
-        object,
-        extract_to_stdout=True,
-        local_path='borg7',
-        remote_path='borg8',
-    ).and_return(extract_process)
-
-    assert module.get_config_paths(
-        'archive',
-        bootstrap_arguments,
-        global_arguments,
-        local_borg_version,
-    ) == ['/borgmatic/config.yaml']
-
-
-def test_get_config_paths_with_missing_manifest_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'get_borgmatic_source_directory',
         'get_borgmatic_source_directory',
     ).and_return('/source')
     ).and_return('/source')
-    flexmock(module).should_receive('make_bootstrap_config').and_return({})
-    bootstrap_arguments = flexmock(
-        repository='repo',
-        archive='archive',
-        ssh_command=None,
-        local_path='borg7',
-        remote_path='borg7',
-        borgmatic_source_directory=None,
-        user_runtime_directory=None,
-    )
-    global_arguments = flexmock(
-        dry_run=False,
-    )
-    local_borg_version = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        flexmock(),
-    )
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'make_runtime_directory_glob',
-    ).replace_with(lambda path: path)
-    flexmock(module.os.path).should_receive('join').and_return('run/borgmatic')
     extract_process = flexmock(stdout=flexmock(read=lambda: ''))
     extract_process = flexmock(stdout=flexmock(read=lambda: ''))
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
         extract_process,
         extract_process,
     )
     )
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
-        module.get_config_paths(
+        module.load_config_paths_from_archive(
+            'repo',
             'archive',
             'archive',
-            bootstrap_arguments,
-            global_arguments,
-            local_borg_version,
+            config={},
+            local_borg_version=flexmock(),
+            global_arguments=flexmock(dry_run=False),
+            borgmatic_runtime_directory='/run',
         )
         )
 
 
 
 
-def test_get_config_paths_with_broken_json_raises_value_error():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_source_directory',
-    ).and_return('/source')
-    flexmock(module).should_receive('make_bootstrap_config').and_return({})
-    bootstrap_arguments = flexmock(
-        repository='repo',
-        archive='archive',
-        ssh_command=None,
-        local_path='borg7',
-        remote_path='borg7',
-        borgmatic_source_directory=None,
-        user_runtime_directory=None,
-    )
-    global_arguments = flexmock(
-        dry_run=False,
-    )
-    local_borg_version = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        flexmock(),
-    )
+def test_load_config_paths_from_archive_with_broken_json_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'make_runtime_directory_glob',
         'make_runtime_directory_glob',
     ).replace_with(lambda path: path)
     ).replace_with(lambda path: path)
+    flexmock(module.borgmatic.config.paths).should_receive(
+        'get_borgmatic_source_directory',
+    ).and_return('/source')
     extract_process = flexmock(
     extract_process = flexmock(
         stdout=flexmock(read=lambda: '{"config_paths": ["/oops'),
         stdout=flexmock(read=lambda: '{"config_paths": ["/oops'),
     )
     )
@@ -235,38 +131,23 @@ def test_get_config_paths_with_broken_json_raises_value_error():
     )
     )
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
-        module.get_config_paths(
+        module.load_config_paths_from_archive(
+            'repo',
             'archive',
             'archive',
-            bootstrap_arguments,
-            global_arguments,
-            local_borg_version,
+            config={},
+            local_borg_version=flexmock(),
+            global_arguments=flexmock(dry_run=False),
+            borgmatic_runtime_directory='/run',
         )
         )
 
 
 
 
-def test_get_config_paths_with_json_missing_key_raises_value_error():
-    flexmock(module.borgmatic.config.paths).should_receive(
-        'get_borgmatic_source_directory',
-    ).and_return('/source')
-    flexmock(module).should_receive('make_bootstrap_config').and_return({})
-    bootstrap_arguments = flexmock(
-        repository='repo',
-        archive='archive',
-        ssh_command=None,
-        local_path='borg7',
-        remote_path='borg7',
-        borgmatic_source_directory=None,
-        user_runtime_directory=None,
-    )
-    global_arguments = flexmock(
-        dry_run=False,
-    )
-    local_borg_version = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        flexmock(),
-    )
+def test_load_config_paths_from_archive_with_json_missing_key_raises_value_error():
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'make_runtime_directory_glob',
         'make_runtime_directory_glob',
     ).replace_with(lambda path: path)
     ).replace_with(lambda path: path)
+    flexmock(module.borgmatic.config.paths).should_receive(
+        'get_borgmatic_source_directory',
+    ).and_return('/source')
     extract_process = flexmock(
     extract_process = flexmock(
         stdout=flexmock(read=lambda: '{}'),
         stdout=flexmock(read=lambda: '{}'),
     )
     )
@@ -275,17 +156,27 @@ def test_get_config_paths_with_json_missing_key_raises_value_error():
     )
     )
 
 
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
-        module.get_config_paths(
+        module.load_config_paths_from_archive(
+            'repo',
             'archive',
             'archive',
-            bootstrap_arguments,
-            global_arguments,
-            local_borg_version,
+            config={},
+            local_borg_version=flexmock(),
+            global_arguments=flexmock(dry_run=False),
+            borgmatic_runtime_directory='/run',
         )
         )
 
 
 
 
 def test_run_bootstrap_does_not_raise():
 def test_run_bootstrap_does_not_raise():
     flexmock(module).should_receive('make_bootstrap_config').and_return({})
     flexmock(module).should_receive('make_bootstrap_config').and_return({})
-    flexmock(module).should_receive('get_config_paths').and_return(['/borgmatic/config.yaml'])
+    flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
+        'archive',
+    )
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock(),
+    )
+    flexmock(module).should_receive('load_config_paths_from_archive').and_return(
+        ['/borgmatic/config.yaml']
+    )
     bootstrap_arguments = flexmock(
     bootstrap_arguments = flexmock(
         repository='repo',
         repository='repo',
         archive='archive',
         archive='archive',
@@ -315,9 +206,6 @@ def test_run_bootstrap_does_not_raise():
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
     flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
         extract_process,
         extract_process,
     ).once()
     ).once()
-    flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
-        'archive',
-    )
 
 
     module.run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version)
     module.run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version)
 
 
@@ -325,7 +213,21 @@ def test_run_bootstrap_does_not_raise():
 def test_run_bootstrap_translates_ssh_command_argument_to_config():
 def test_run_bootstrap_translates_ssh_command_argument_to_config():
     config = {}
     config = {}
     flexmock(module).should_receive('make_bootstrap_config').and_return(config)
     flexmock(module).should_receive('make_bootstrap_config').and_return(config)
-    flexmock(module).should_receive('get_config_paths').and_return(['/borgmatic/config.yaml'])
+    flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').with_args(
+        'repo',
+        'archive',
+        config,
+        object,
+        object,
+        local_path='borg7',
+        remote_path='borg8',
+    ).and_return('archive')
+    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
+        flexmock(),
+    )
+    flexmock(module).should_receive('load_config_paths_from_archive').and_return(
+        ['/borgmatic/config.yaml']
+    )
     bootstrap_arguments = flexmock(
     bootstrap_arguments = flexmock(
         repository='repo',
         repository='repo',
         archive='archive',
         archive='archive',
@@ -341,9 +243,6 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
         dry_run=False,
         dry_run=False,
     )
     )
     local_borg_version = flexmock()
     local_borg_version = flexmock()
-    flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
-        flexmock(),
-    )
     flexmock(module.borgmatic.config.paths).should_receive(
     flexmock(module.borgmatic.config.paths).should_receive(
         'make_runtime_directory_glob',
         'make_runtime_directory_glob',
     ).replace_with(lambda path: path)
     ).replace_with(lambda path: path)
@@ -366,14 +265,5 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
         local_path='borg7',
         local_path='borg7',
         remote_path='borg8',
         remote_path='borg8',
     ).and_return(extract_process).once()
     ).and_return(extract_process).once()
-    flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').with_args(
-        'repo',
-        'archive',
-        config,
-        object,
-        object,
-        local_path='borg7',
-        remote_path='borg8',
-    ).and_return('archive')
 
 
     module.run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version)
     module.run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version)

+ 33 - 8
tests/unit/actions/test_check.py

@@ -580,7 +580,7 @@ def test_upgrade_check_times_renames_stale_temporary_check_path():
     module.upgrade_check_times(flexmock(), flexmock())
     module.upgrade_check_times(flexmock(), flexmock())
 
 
 
 
-def test_collect_spot_check_source_paths_parses_borg_output():
+def test_collect_spot_check_source_paths_parses_borg_output_and_includes_bootstrap_config_paths():
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
     flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return(
         {'hook1': False, 'hook2': True},
         {'hook1': False, 'hook2': True},
     )
     )
@@ -588,9 +588,16 @@ def test_collect_spot_check_source_paths_parses_borg_output():
         flexmock(),
         flexmock(),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
-        flexmock(),
+        (Pattern('collected'),),
     )
     )
-    flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
+    flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').with_args(
+        (
+            Pattern('collected', source=module.borgmatic.borg.pattern.Pattern_source.HOOK),
+            Pattern('extra.yaml', source=module.borgmatic.borg.pattern.Pattern_source.INTERNAL),
+        ),
+        config=object,
+        working_directory=None,
+    ).and_return(
         [Pattern('foo'), Pattern('bar')],
         [Pattern('foo'), Pattern('bar')],
     )
     )
     flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
     flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
@@ -624,6 +631,7 @@ def test_collect_spot_check_source_paths_parses_borg_output():
         local_path=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
         remote_path=flexmock(),
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
+        bootstrap_config_paths=('extra.yaml',),
     ) == ('/etc/path', '/etc/other')
     ) == ('/etc/path', '/etc/other')
 
 
 
 
@@ -635,7 +643,7 @@ def test_collect_spot_check_source_paths_omits_progress_from_create_dry_run_comm
         flexmock(),
         flexmock(),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
-        flexmock(),
+        (Pattern('collected'),),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
         [Pattern('foo'), Pattern('bar')],
         [Pattern('foo'), Pattern('bar')],
@@ -671,6 +679,7 @@ def test_collect_spot_check_source_paths_omits_progress_from_create_dry_run_comm
         local_path=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
         remote_path=flexmock(),
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
+        bootstrap_config_paths=(),
     ) == ('/etc/path', '/etc/other')
     ) == ('/etc/path', '/etc/other')
 
 
 
 
@@ -682,7 +691,7 @@ def test_collect_spot_check_source_paths_passes_through_stream_processes_false()
         flexmock(),
         flexmock(),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
-        flexmock(),
+        (Pattern('collected'),),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
         [Pattern('foo'), Pattern('bar')],
         [Pattern('foo'), Pattern('bar')],
@@ -718,6 +727,7 @@ def test_collect_spot_check_source_paths_passes_through_stream_processes_false()
         local_path=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
         remote_path=flexmock(),
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
+        bootstrap_config_paths=(),
     ) == ('/etc/path', '/etc/other')
     ) == ('/etc/path', '/etc/other')
 
 
 
 
@@ -729,7 +739,7 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o
         flexmock(),
         flexmock(),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
-        flexmock(),
+        (Pattern('collected'),),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
         [Pattern('foo'), Pattern('bar')],
         [Pattern('foo'), Pattern('bar')],
@@ -765,6 +775,7 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o
         local_path=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
         remote_path=flexmock(),
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
+        bootstrap_config_paths=(),
     ) == ('/etc/path', '/etc/other')
     ) == ('/etc/path', '/etc/other')
 
 
 
 
@@ -776,7 +787,7 @@ def test_collect_spot_check_source_paths_skips_directories():
         flexmock(),
         flexmock(),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
-        flexmock(),
+        (Pattern('collected'),),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
         [Pattern('foo'), Pattern('bar')],
         [Pattern('foo'), Pattern('bar')],
@@ -814,6 +825,7 @@ def test_collect_spot_check_source_paths_skips_directories():
             local_path=flexmock(),
             local_path=flexmock(),
             remote_path=flexmock(),
             remote_path=flexmock(),
             borgmatic_runtime_directory='/run/borgmatic',
             borgmatic_runtime_directory='/run/borgmatic',
+            bootstrap_config_paths=(),
         )
         )
         == ()
         == ()
     )
     )
@@ -921,7 +933,7 @@ def test_collect_spot_check_source_paths_uses_working_directory():
         flexmock(),
         flexmock(),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(
-        flexmock(),
+        (Pattern('collected'),),
     )
     )
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
     flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return(
         [Pattern('foo'), Pattern('bar')],
         [Pattern('foo'), Pattern('bar')],
@@ -960,6 +972,7 @@ def test_collect_spot_check_source_paths_uses_working_directory():
         local_path=flexmock(),
         local_path=flexmock(),
         remote_path=flexmock(),
         remote_path=flexmock(),
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
+        bootstrap_config_paths=(),
     ) == ('foo', 'bar')
     ) == ('foo', 'bar')
 
 
 
 
@@ -1384,6 +1397,9 @@ def test_spot_check_with_count_delta_greater_than_count_tolerance_percentage_err
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         'archive',
         'archive',
     )
     )
+    flexmock(module.borgmatic.actions.config.bootstrap).should_receive(
+        'load_config_paths_from_archive'
+    ).and_return(('bootstrap.yaml',))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(
         ('/foo', '/bar'),
         ('/foo', '/bar'),
     ).once()
     ).once()
@@ -1416,6 +1432,9 @@ def test_spot_check_with_failing_percentage_greater_than_data_tolerance_percenta
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         'archive',
         'archive',
     )
     )
+    flexmock(module.borgmatic.actions.config.bootstrap).should_receive(
+        'load_config_paths_from_archive'
+    ).and_return(('bootstrap.yaml',))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
     flexmock(module).should_receive('compare_spot_check_hashes').and_return(
     flexmock(module).should_receive('compare_spot_check_hashes').and_return(
         ('/bar', '/baz', '/quux'),
         ('/bar', '/baz', '/quux'),
@@ -1449,6 +1468,9 @@ def test_spot_check_with_high_enough_tolerances_does_not_raise():
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         'archive',
         'archive',
     )
     )
+    flexmock(module.borgmatic.actions.config.bootstrap).should_receive(
+        'load_config_paths_from_archive'
+    ).and_return(('bootstrap.yaml',))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
     flexmock(module).should_receive('compare_spot_check_hashes').and_return(
     flexmock(module).should_receive('compare_spot_check_hashes').and_return(
         ('/bar', '/baz', '/quux'),
         ('/bar', '/baz', '/quux'),
@@ -1479,6 +1501,9 @@ def test_spot_check_without_any_source_paths_errors():
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
     flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
         'archive',
         'archive',
     )
     )
+    flexmock(module.borgmatic.actions.config.bootstrap).should_receive(
+        'load_config_paths_from_archive'
+    ).and_return(('bootstrap.yaml',))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
     flexmock(module).should_receive('collect_spot_check_archive_paths').and_return(('/foo', '/bar'))
     flexmock(module).should_receive('compare_spot_check_hashes').never()
     flexmock(module).should_receive('compare_spot_check_hashes').never()