ansible-core 2.13.8__py3-none-any.whl → 2.13.9rc1__py3-none-any.whl

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.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (43) hide show
  1. ansible/cli/doc.py +10 -4
  2. ansible/galaxy/api.py +29 -10
  3. ansible/galaxy/collection/__init__.py +3 -0
  4. ansible/galaxy/collection/concrete_artifact_manager.py +34 -17
  5. ansible/galaxy/dependency_resolution/dataclasses.py +11 -1
  6. ansible/galaxy/dependency_resolution/providers.py +0 -1
  7. ansible/module_utils/ansible_release.py +1 -1
  8. ansible/module_utils/api.py +14 -1
  9. ansible/module_utils/csharp/Ansible.Basic.cs +265 -7
  10. ansible/plugins/lookup/password.py +79 -39
  11. ansible/release.py +1 -1
  12. ansible/utils/encrypt.py +9 -6
  13. {ansible_core-2.13.8.dist-info → ansible_core-2.13.9rc1.dist-info}/METADATA +1 -1
  14. {ansible_core-2.13.8.dist-info → ansible_core-2.13.9rc1.dist-info}/RECORD +43 -43
  15. {ansible_core-2.13.8.dist-info → ansible_core-2.13.9rc1.dist-info}/WHEEL +1 -1
  16. ansible_test/_internal/ci/__init__.py +2 -2
  17. ansible_test/_internal/ci/azp.py +12 -8
  18. ansible_test/_internal/ci/local.py +2 -2
  19. ansible_test/_internal/classification/__init__.py +51 -43
  20. ansible_test/_internal/cli/argparsing/argcompletion.py +20 -5
  21. ansible_test/_internal/cli/commands/sanity.py +0 -15
  22. ansible_test/_internal/commands/coverage/combine.py +3 -1
  23. ansible_test/_internal/commands/integration/__init__.py +6 -2
  24. ansible_test/_internal/commands/integration/cloud/__init__.py +3 -1
  25. ansible_test/_internal/commands/sanity/__init__.py +7 -0
  26. ansible_test/_internal/commands/sanity/pylint.py +11 -0
  27. ansible_test/_internal/commands/sanity/validate_modules.py +66 -5
  28. ansible_test/_internal/config.py +6 -12
  29. ansible_test/_internal/core_ci.py +8 -1
  30. ansible_test/_internal/data.py +17 -8
  31. ansible_test/_internal/delegation.py +1 -2
  32. ansible_test/_internal/metadata.py +4 -0
  33. ansible_test/_internal/payload.py +75 -6
  34. ansible_test/_internal/python_requirements.py +15 -0
  35. ansible_test/_internal/target.py +3 -7
  36. ansible_test/_internal/test.py +1 -1
  37. ansible_test/_internal/util.py +17 -0
  38. ansible_test/_util/controller/sanity/mypy/ansible-test.ini +3 -0
  39. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +92 -126
  40. {ansible_core-2.13.8.data → ansible_core-2.13.9rc1.data}/scripts/ansible-test +0 -0
  41. {ansible_core-2.13.8.dist-info → ansible_core-2.13.9rc1.dist-info}/COPYING +0 -0
  42. {ansible_core-2.13.8.dist-info → ansible_core-2.13.9rc1.dist-info}/entry_points.txt +0 -0
  43. {ansible_core-2.13.8.dist-info → ansible_core-2.13.9rc1.dist-info}/top_level.txt +0 -0
@@ -661,21 +661,58 @@ class PathMapper:
661
661
 
662
662
  def _classify_ansible(self, path): # type: (str) -> t.Optional[t.Dict[str, str]]
663
663
  """Return the classification for the given path using rules specific to Ansible."""
664
+ dirname = os.path.dirname(path)
665
+ filename = os.path.basename(path)
666
+ name, ext = os.path.splitext(filename)
667
+
668
+ minimal: dict[str, str] = {}
669
+
670
+ packaging = {
671
+ 'integration': 'packaging/',
672
+ }
673
+
674
+ # Early classification that needs to occur before common classification belongs here.
675
+
664
676
  if path.startswith('test/units/compat/'):
665
677
  return {
666
678
  'units': 'test/units/',
667
679
  }
668
680
 
681
+ if dirname == '.azure-pipelines/commands':
682
+ test_map = {
683
+ 'cloud.sh': 'integration:cloud/',
684
+ 'linux.sh': 'integration:all',
685
+ 'network.sh': 'network-integration:all',
686
+ 'remote.sh': 'integration:all',
687
+ 'sanity.sh': 'sanity:all',
688
+ 'units.sh': 'units:all',
689
+ 'windows.sh': 'windows-integration:all',
690
+ }
691
+
692
+ test_match = test_map.get(filename)
693
+
694
+ if test_match:
695
+ test_command, test_target = test_match.split(':')
696
+
697
+ return {
698
+ test_command: test_target,
699
+ }
700
+
701
+ cloud_target = f'cloud/{name}/'
702
+
703
+ if cloud_target in self.integration_targets_by_alias:
704
+ return {
705
+ 'integration': cloud_target,
706
+ }
707
+
708
+ # Classification common to both ansible and collections.
709
+
669
710
  result = self._classify_common(path)
670
711
 
671
712
  if result is not None:
672
713
  return result
673
714
 
674
- dirname = os.path.dirname(path)
675
- filename = os.path.basename(path)
676
- name, ext = os.path.splitext(filename)
677
-
678
- minimal = {} # type: t.Dict[str, str]
715
+ # Classification here is specific to ansible, and runs after common classification.
679
716
 
680
717
  if path.startswith('bin/'):
681
718
  return all_tests(self.args) # broad impact, run all tests
@@ -715,6 +752,9 @@ class PathMapper:
715
752
  return minimal
716
753
 
717
754
  if path.startswith('packaging/'):
755
+ if path.startswith('packaging/pep517_backend/'):
756
+ return packaging
757
+
718
758
  return minimal
719
759
 
720
760
  if path.startswith('test/ansible_test/'):
@@ -791,39 +831,6 @@ class PathMapper:
791
831
  if path.startswith('test/support/'):
792
832
  return all_tests(self.args) # test infrastructure, run all tests
793
833
 
794
- if path.startswith('test/utils/shippable/'):
795
- if dirname == 'test/utils/shippable':
796
- test_map = {
797
- 'cloud.sh': 'integration:cloud/',
798
- 'linux.sh': 'integration:all',
799
- 'network.sh': 'network-integration:all',
800
- 'remote.sh': 'integration:all',
801
- 'sanity.sh': 'sanity:all',
802
- 'units.sh': 'units:all',
803
- 'windows.sh': 'windows-integration:all',
804
- }
805
-
806
- test_match = test_map.get(filename)
807
-
808
- if test_match:
809
- test_command, test_target = test_match.split(':')
810
-
811
- return {
812
- test_command: test_target,
813
- }
814
-
815
- cloud_target = 'cloud/%s/' % name
816
-
817
- if cloud_target in self.integration_targets_by_alias:
818
- return {
819
- 'integration': cloud_target,
820
- }
821
-
822
- return all_tests(self.args) # test infrastructure, run all tests
823
-
824
- if path.startswith('test/utils/'):
825
- return minimal
826
-
827
834
  if '/' not in path:
828
835
  if path in (
829
836
  '.gitattributes',
@@ -835,16 +842,17 @@ class PathMapper:
835
842
  return minimal
836
843
 
837
844
  if path in (
838
- 'setup.py',
845
+ 'MANIFEST.in',
846
+ 'pyproject.toml',
847
+ 'requirements.txt',
848
+ 'setup.cfg',
849
+ 'setup.py',
839
850
  ):
840
- return all_tests(self.args) # broad impact, run all tests
851
+ return packaging
841
852
 
842
853
  if ext in (
843
- '.in',
844
854
  '.md',
845
855
  '.rst',
846
- '.toml',
847
- '.txt',
848
856
  ):
849
857
  return minimal
850
858
 
@@ -16,10 +16,19 @@ class Substitute:
16
16
  try:
17
17
  import argcomplete
18
18
 
19
- from argcomplete import (
20
- CompletionFinder,
21
- default_validator,
22
- )
19
+ try:
20
+ # argcomplete 3+
21
+ # see: https://github.com/kislyuk/argcomplete/commit/bd781cb08512b94966312377186ebc5550f46ae0
22
+ from argcomplete.finders import (
23
+ CompletionFinder,
24
+ default_validator,
25
+ )
26
+ except ImportError:
27
+ # argcomplete <3
28
+ from argcomplete import (
29
+ CompletionFinder,
30
+ default_validator,
31
+ )
23
32
 
24
33
  warn = argcomplete.warn # pylint: disable=invalid-name
25
34
  except ImportError:
@@ -70,7 +79,13 @@ class CompType(enum.Enum):
70
79
  def register_safe_action(action_type): # type: (t.Type[argparse.Action]) -> None
71
80
  """Register the given action as a safe action for argcomplete to use during completion if it is not already registered."""
72
81
  if argcomplete and action_type not in argcomplete.safe_actions:
73
- argcomplete.safe_actions += (action_type,)
82
+ if isinstance(argcomplete.safe_actions, set):
83
+ # argcomplete 3+
84
+ # see: https://github.com/kislyuk/argcomplete/commit/bd781cb08512b94966312377186ebc5550f46ae0
85
+ argcomplete.safe_actions.add(action_type)
86
+ else:
87
+ # argcomplete <3
88
+ argcomplete.safe_actions += (action_type,)
74
89
 
75
90
 
76
91
  def get_comp_type(): # type: () -> t.Optional[CompType]
@@ -16,10 +16,6 @@ from ...target import (
16
16
  walk_sanity_targets,
17
17
  )
18
18
 
19
- from ...data import (
20
- data_context,
21
- )
22
-
23
19
  from ..environments import (
24
20
  CompositeActionCompletionFinder,
25
21
  ControllerMode,
@@ -81,17 +77,6 @@ def do_sanity(
81
77
  help='enable optional errors',
82
78
  )
83
79
 
84
- if data_context().content.is_ansible:
85
- sanity.add_argument(
86
- '--keep-git',
87
- action='store_true',
88
- help='transfer git related files to the remote host/container',
89
- )
90
- else:
91
- sanity.set_defaults(
92
- keep_git=False,
93
- )
94
-
95
80
  sanity.add_argument(
96
81
  '--lint',
97
82
  action='store_true',
@@ -33,6 +33,7 @@ from ...executor import (
33
33
 
34
34
  from ...data import (
35
35
  data_context,
36
+ PayloadConfig,
36
37
  )
37
38
 
38
39
  from ...host_configs import (
@@ -81,9 +82,10 @@ def combine_coverage_files(args, host_state): # type: (CoverageCombineConfig, H
81
82
 
82
83
  pairs = [(path, os.path.relpath(path, data_context().content.root)) for path in exported_paths]
83
84
 
84
- def coverage_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
85
+ def coverage_callback(payload_config: PayloadConfig) -> None:
85
86
  """Add the coverage files to the payload file list."""
86
87
  display.info('Including %d exported coverage file(s) in payload.' % len(pairs), verbosity=1)
88
+ files = payload_config.files
87
89
  files.extend(pairs)
88
90
 
89
91
  data_context().register_payload_callback(coverage_callback)
@@ -89,6 +89,7 @@ from .cloud import (
89
89
 
90
90
  from ...data import (
91
91
  data_context,
92
+ PayloadConfig,
92
93
  )
93
94
 
94
95
  from ...host_configs import (
@@ -213,11 +214,13 @@ def delegate_inventory(args, inventory_path_src): # type: (IntegrationConfig, s
213
214
  if isinstance(args, PosixIntegrationConfig):
214
215
  return
215
216
 
216
- def inventory_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
217
+ def inventory_callback(payload_config: PayloadConfig) -> None:
217
218
  """
218
219
  Add the inventory file to the payload file list.
219
220
  This will preserve the file during delegation even if it is ignored or is outside the content and install roots.
220
221
  """
222
+ files = payload_config.files
223
+
221
224
  inventory_path = get_inventory_relative_path(args)
222
225
  inventory_tuple = inventory_path_src, inventory_path
223
226
 
@@ -940,11 +943,12 @@ def command_integration_filter(args, # type: TIntegrationConfig
940
943
  vars_file_src = os.path.join(data_context().content.root, data_context().content.integration_vars_path)
941
944
 
942
945
  if os.path.exists(vars_file_src):
943
- def integration_config_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
946
+ def integration_config_callback(payload_config: PayloadConfig) -> None:
944
947
  """
945
948
  Add the integration config vars file to the payload file list.
946
949
  This will preserve the file during delegation even if the file is ignored by source control.
947
950
  """
951
+ files = payload_config.files
948
952
  files.append((vars_file_src, data_context().content.integration_vars_path))
949
953
 
950
954
  data_context().register_payload_callback(integration_config_callback)
@@ -47,6 +47,7 @@ from ....ci import (
47
47
 
48
48
  from ....data import (
49
49
  data_context,
50
+ PayloadConfig,
50
51
  )
51
52
 
52
53
  from ....docker_util import (
@@ -189,13 +190,14 @@ class CloudBase(metaclass=abc.ABCMeta):
189
190
  self.args = args
190
191
  self.platform = self.__module__.rsplit('.', 1)[-1]
191
192
 
192
- def config_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
193
+ def config_callback(payload_config: PayloadConfig) -> None:
193
194
  """Add the config file to the payload file list."""
194
195
  if self.platform not in self.args.metadata.cloud_config:
195
196
  return # platform was initialized, but not used -- such as being skipped due to all tests being disabled
196
197
 
197
198
  if self._get_cloud_config(self._CONFIG_PATH, ''):
198
199
  pair = (self.config_path, os.path.relpath(self.config_path, data_context().content.root))
200
+ files = payload_config.files
199
201
 
200
202
  if pair not in files:
201
203
  display.info('Including %s config: %s -> %s' % (self.platform, pair[0], pair[1]), verbosity=3)
@@ -159,6 +159,10 @@ def command_sanity(args): # type: (SanityConfig) -> None
159
159
  if args.skip_test:
160
160
  tests = [target for target in tests if target.name not in args.skip_test]
161
161
 
162
+ if not args.host_path:
163
+ for test in tests:
164
+ test.origin_hook(args)
165
+
162
166
  targets_use_pypi = any(isinstance(test, SanityMultipleVersion) and test.needs_pypi for test in tests) and not args.list_tests
163
167
  host_state = prepare_profiles(args, targets_use_pypi=targets_use_pypi) # sanity
164
168
 
@@ -756,6 +760,9 @@ class SanityTest(metaclass=abc.ABCMeta):
756
760
  """A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
757
761
  return CONTROLLER_PYTHON_VERSIONS
758
762
 
763
+ def origin_hook(self, args: SanityConfig) -> None:
764
+ """This method is called on the origin, before the test runs or delegation occurs."""
765
+
759
766
  def filter_targets(self, targets): # type: (t.List[TestTarget]) -> t.List[TestTarget] # pylint: disable=unused-argument
760
767
  """Return the given list of test targets, filtered to include only those relevant for the test."""
761
768
  if self.no_targets:
@@ -17,6 +17,10 @@ from . import (
17
17
  SANITY_ROOT,
18
18
  )
19
19
 
20
+ from ...io import (
21
+ make_dirs,
22
+ )
23
+
20
24
  from ...test import (
21
25
  TestResult,
22
26
  )
@@ -40,6 +44,7 @@ from ...ansible_util import (
40
44
  get_collection_detail,
41
45
  CollectionDetail,
42
46
  CollectionDetailError,
47
+ ResultType,
43
48
  )
44
49
 
45
50
  from ...config import (
@@ -245,6 +250,12 @@ class PylintTest(SanitySingleVersion):
245
250
  # expose plugin paths for use in custom plugins
246
251
  env.update(dict(('ANSIBLE_TEST_%s_PATH' % k.upper(), os.path.abspath(v) + os.path.sep) for k, v in data_context().content.plugin_paths.items()))
247
252
 
253
+ # Set PYLINTHOME to prevent pylint from checking for an obsolete directory, which can result in a test failure due to stderr output.
254
+ # See: https://github.com/PyCQA/pylint/blob/e6c6bf5dfd61511d64779f54264b27a368c43100/pylint/constants.py#L148
255
+ pylint_home = os.path.join(ResultType.TMP.path, 'pylint')
256
+ make_dirs(pylint_home)
257
+ env.update(PYLINTHOME=pylint_home)
258
+
248
259
  if paths:
249
260
  display.info('Checking %d file(s) in context "%s" with config: %s' % (len(paths), context, rcfile), verbosity=1)
250
261
 
@@ -1,9 +1,12 @@
1
1
  """Sanity test using validate-modules."""
2
2
  from __future__ import annotations
3
3
 
4
+ import atexit
4
5
  import collections
6
+ import contextlib
5
7
  import json
6
8
  import os
9
+ import tarfile
7
10
  import typing as t
8
11
 
9
12
  from . import (
@@ -16,6 +19,10 @@ from . import (
16
19
  SANITY_ROOT,
17
20
  )
18
21
 
22
+ from ...io import (
23
+ make_dirs,
24
+ )
25
+
19
26
  from ...test import (
20
27
  TestResult,
21
28
  )
@@ -30,7 +37,9 @@ from ...util import (
30
37
  )
31
38
 
32
39
  from ...util_common import (
40
+ process_scoped_temporary_directory,
33
41
  run_command,
42
+ ResultType,
34
43
  )
35
44
 
36
45
  from ...ansible_util import (
@@ -49,12 +58,21 @@ from ...ci import (
49
58
 
50
59
  from ...data import (
51
60
  data_context,
61
+ PayloadConfig,
52
62
  )
53
63
 
54
64
  from ...host_configs import (
55
65
  PythonConfig,
56
66
  )
57
67
 
68
+ from ...git import (
69
+ Git,
70
+ )
71
+
72
+ from ...provider.source import (
73
+ SourceProvider as GitSourceProvider,
74
+ )
75
+
58
76
 
59
77
  class ValidateModulesTest(SanitySingleVersion):
60
78
  """Sanity test using validate-modules."""
@@ -130,14 +148,17 @@ class ValidateModulesTest(SanitySingleVersion):
130
148
  except CollectionDetailError as ex:
131
149
  display.warning('Skipping validate-modules collection version checks since collection detail loading failed: %s' % ex.reason)
132
150
  else:
133
- base_branch = args.base_branch or get_ci_provider().get_base_branch()
151
+ path = self.get_archive_path(args)
152
+
153
+ if os.path.exists(path):
154
+ temp_dir = process_scoped_temporary_directory(args)
155
+
156
+ with tarfile.open(path) as file:
157
+ file.extractall(temp_dir)
134
158
 
135
- if base_branch:
136
159
  cmd.extend([
137
- '--base-branch', base_branch,
160
+ '--original-plugins', temp_dir,
138
161
  ])
139
- else:
140
- display.warning('Cannot perform module comparison against the base branch because the base branch was not detected.')
141
162
 
142
163
  errors = []
143
164
 
@@ -188,3 +209,43 @@ class ValidateModulesTest(SanitySingleVersion):
188
209
  return SanityFailure(self.name, messages=all_errors)
189
210
 
190
211
  return SanitySuccess(self.name)
212
+
213
+ def origin_hook(self, args: SanityConfig) -> None:
214
+ """This method is called on the origin, before the test runs or delegation occurs."""
215
+ if not data_context().content.is_ansible:
216
+ return
217
+
218
+ if not isinstance(data_context().source_provider, GitSourceProvider):
219
+ display.warning('The validate-modules sanity test cannot compare against the base commit because git is not being used.')
220
+ return
221
+
222
+ base_commit = args.base_branch or get_ci_provider().get_base_commit(args)
223
+
224
+ if not base_commit:
225
+ display.warning('The validate-modules sanity test cannot compare against the base commit because it was not detected.')
226
+ return
227
+
228
+ path = self.get_archive_path(args)
229
+
230
+ def cleanup() -> None:
231
+ """Cleanup callback called when the process exits."""
232
+ with contextlib.suppress(FileNotFoundError):
233
+ os.unlink(path)
234
+
235
+ def git_callback(payload_config: PayloadConfig) -> None:
236
+ """Include the previous plugin content archive in the payload."""
237
+ files = payload_config.files
238
+ files.append((path, os.path.relpath(path, data_context().content.root)))
239
+
240
+ atexit.register(cleanup)
241
+ data_context().register_payload_callback(git_callback)
242
+
243
+ make_dirs(os.path.dirname(path))
244
+
245
+ git = Git()
246
+ git.run_git(['archive', '--output', path, base_commit, 'lib/ansible/modules/', 'lib/ansible/plugins/'])
247
+
248
+ @staticmethod
249
+ def get_archive_path(args: SanityConfig) -> str:
250
+ """Return the path to the original plugin content archive."""
251
+ return os.path.join(ResultType.TMP.path, f'validate-modules-{args.metadata.session_id}.tar')
@@ -24,6 +24,7 @@ from .metadata import (
24
24
 
25
25
  from .data import (
26
26
  data_context,
27
+ PayloadConfig,
27
28
  )
28
29
 
29
30
  from .host_configs import (
@@ -114,7 +115,7 @@ class EnvironmentConfig(CommonConfig):
114
115
  self.dev_systemd_debug: bool = args.dev_systemd_debug
115
116
  self.dev_probe_cgroups: t.Optional[str] = args.dev_probe_cgroups
116
117
 
117
- def host_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
118
+ def host_callback(payload_config: PayloadConfig) -> None:
118
119
  """Add the host files to the payload file list."""
119
120
  config = self
120
121
 
@@ -123,6 +124,8 @@ class EnvironmentConfig(CommonConfig):
123
124
  state_path = os.path.join(config.host_path, 'state.dat')
124
125
  config_path = os.path.join(config.host_path, 'config.dat')
125
126
 
127
+ files = payload_config.files
128
+
126
129
  files.append((os.path.abspath(settings_path), settings_path))
127
130
  files.append((os.path.abspath(state_path), state_path))
128
131
  files.append((os.path.abspath(config_path), config_path))
@@ -225,9 +228,10 @@ class TestConfig(EnvironmentConfig):
225
228
  if self.coverage_check:
226
229
  self.coverage = True
227
230
 
228
- def metadata_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
231
+ def metadata_callback(payload_config: PayloadConfig) -> None:
229
232
  """Add the metadata file to the payload file list."""
230
233
  config = self
234
+ files = payload_config.files
231
235
 
232
236
  if config.metadata_path:
233
237
  files.append((os.path.abspath(config.metadata_path), config.metadata_path))
@@ -258,20 +262,10 @@ class SanityConfig(TestConfig):
258
262
  self.list_tests = args.list_tests # type: bool
259
263
  self.allow_disabled = args.allow_disabled # type: bool
260
264
  self.enable_optional_errors = args.enable_optional_errors # type: bool
261
- self.keep_git = args.keep_git # type: bool
262
265
  self.prime_venvs = args.prime_venvs # type: bool
263
266
 
264
267
  self.display_stderr = self.lint or self.list_tests
265
268
 
266
- if self.keep_git:
267
- def git_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
268
- """Add files from the content root .git directory to the payload file list."""
269
- for dirpath, _dirnames, filenames in os.walk(os.path.join(data_context().content.root, '.git')):
270
- paths = [os.path.join(dirpath, filename) for filename in filenames]
271
- files.extend((path, os.path.relpath(path, data_context().content.root)) for path in paths)
272
-
273
- data_context().register_payload_callback(git_callback)
274
-
275
269
 
276
270
  class IntegrationConfig(TestConfig):
277
271
  """Configuration for the integration command."""
@@ -6,6 +6,7 @@ import dataclasses
6
6
  import json
7
7
  import os
8
8
  import re
9
+ import stat
9
10
  import traceback
10
11
  import uuid
11
12
  import errno
@@ -47,6 +48,7 @@ from .ci import (
47
48
 
48
49
  from .data import (
49
50
  data_context,
51
+ PayloadConfig,
50
52
  )
51
53
 
52
54
 
@@ -447,14 +449,19 @@ class SshKey:
447
449
  key, pub = key_pair
448
450
  key_dst, pub_dst = self.get_in_tree_key_pair_paths()
449
451
 
450
- def ssh_key_callback(files): # type: (t.List[t.Tuple[str, str]]) -> None
452
+ def ssh_key_callback(payload_config: PayloadConfig) -> None:
451
453
  """
452
454
  Add the SSH keys to the payload file list.
453
455
  They are either outside the source tree or in the cache dir which is ignored by default.
454
456
  """
457
+ files = payload_config.files
458
+ permissions = payload_config.permissions
459
+
455
460
  files.append((key, os.path.relpath(key_dst, data_context().content.root)))
456
461
  files.append((pub, os.path.relpath(pub_dst, data_context().content.root)))
457
462
 
463
+ permissions[os.path.relpath(key_dst, data_context().content.root)] = stat.S_IRUSR | stat.S_IWUSR
464
+
458
465
  data_context().register_payload_callback(ssh_key_callback)
459
466
 
460
467
  self.key, self.pub = key, pub
@@ -1,6 +1,7 @@
1
1
  """Context information for the current invocation of ansible-test."""
2
2
  from __future__ import annotations
3
3
 
4
+ import collections.abc as c
4
5
  import dataclasses
5
6
  import os
6
7
  import typing as t
@@ -49,6 +50,13 @@ from .provider.layout.unsupported import (
49
50
  )
50
51
 
51
52
 
53
+ @dataclasses.dataclass(frozen=True)
54
+ class PayloadConfig:
55
+ """Configuration required to build a source tree payload for delegation."""
56
+ files: list[tuple[str, str]]
57
+ permissions: dict[str, int]
58
+
59
+
52
60
  class DataContext:
53
61
  """Data context providing details about the current execution environment for ansible-test."""
54
62
  def __init__(self):
@@ -62,16 +70,17 @@ class DataContext:
62
70
  self.__source_providers = source_providers
63
71
  self.__ansible_source = None # type: t.Optional[t.Tuple[t.Tuple[str, str], ...]]
64
72
 
65
- self.payload_callbacks = [] # type: t.List[t.Callable[[t.List[t.Tuple[str, str]]], None]]
73
+ self.payload_callbacks: list[c.Callable[[PayloadConfig], None]] = []
66
74
 
67
75
  if content_path:
68
- content = self.__create_content_layout(layout_providers, source_providers, content_path, False)
76
+ content, source_provider = self.__create_content_layout(layout_providers, source_providers, content_path, False)
69
77
  elif ANSIBLE_SOURCE_ROOT and is_subdir(current_path, ANSIBLE_SOURCE_ROOT):
70
- content = self.__create_content_layout(layout_providers, source_providers, ANSIBLE_SOURCE_ROOT, False)
78
+ content, source_provider = self.__create_content_layout(layout_providers, source_providers, ANSIBLE_SOURCE_ROOT, False)
71
79
  else:
72
- content = self.__create_content_layout(layout_providers, source_providers, current_path, True)
80
+ content, source_provider = self.__create_content_layout(layout_providers, source_providers, current_path, True)
73
81
 
74
82
  self.content = content # type: ContentLayout
83
+ self.source_provider = source_provider
75
84
 
76
85
  def create_collection_layouts(self): # type: () -> t.List[ContentLayout]
77
86
  """
@@ -99,7 +108,7 @@ class DataContext:
99
108
  if collection_path == os.path.join(collection.root, collection.directory):
100
109
  collection_layout = layout
101
110
  else:
102
- collection_layout = self.__create_content_layout(self.__layout_providers, self.__source_providers, collection_path, False)
111
+ collection_layout = self.__create_content_layout(self.__layout_providers, self.__source_providers, collection_path, False)[0]
103
112
 
104
113
  file_count = len(collection_layout.all_files())
105
114
 
@@ -116,7 +125,7 @@ class DataContext:
116
125
  source_providers, # type: t.List[t.Type[SourceProvider]]
117
126
  root, # type: str
118
127
  walk, # type: bool
119
- ): # type: (...) -> ContentLayout
128
+ ) -> t.Tuple[ContentLayout, SourceProvider]:
120
129
  """Create a content layout using the given providers and root path."""
121
130
  try:
122
131
  layout_provider = find_path_provider(LayoutProvider, layout_providers, root, walk)
@@ -137,7 +146,7 @@ class DataContext:
137
146
 
138
147
  layout = layout_provider.create(layout_provider.root, source_provider.get_paths(layout_provider.root))
139
148
 
140
- return layout
149
+ return layout, source_provider
141
150
 
142
151
  def __create_ansible_source(self):
143
152
  """Return a tuple of Ansible source files with both absolute and relative paths."""
@@ -172,7 +181,7 @@ class DataContext:
172
181
 
173
182
  return self.__ansible_source
174
183
 
175
- def register_payload_callback(self, callback): # type: (t.Callable[[t.List[t.Tuple[str, str]]], None]) -> None
184
+ def register_payload_callback(self, callback: c.Callable[[PayloadConfig], None]) -> None:
176
185
  """Register the given payload callback."""
177
186
  self.payload_callbacks.append(callback)
178
187
 
@@ -172,7 +172,6 @@ def delegate_command(args, host_state, exclude, require): # type: (EnvironmentC
172
172
  con.run(['mkdir', '-p'] + writable_dirs, capture=True)
173
173
  con.run(['chmod', '777'] + writable_dirs, capture=True)
174
174
  con.run(['chmod', '755', working_directory], capture=True)
175
- con.run(['chmod', '644', os.path.join(content_root, args.metadata_path)], capture=True)
176
175
  con.run(['useradd', pytest_user, '--create-home'], capture=True)
177
176
 
178
177
  con.run(insert_options(command, options + ['--requirements-mode', 'only']), capture=False)
@@ -339,7 +338,7 @@ def filter_options(
339
338
  ('--metadata', 1, args.metadata_path),
340
339
  ('--exclude', 1, exclude),
341
340
  ('--require', 1, require),
342
- ('--base-branch', 1, args.base_branch or get_ci_provider().get_base_branch()),
341
+ ('--base-branch', 1, False),
343
342
  ])
344
343
 
345
344
  pass_through_args: list[str] = []