mirror of
https://github.com/ansible/ansible.git
synced 2026-02-03 20:40:24 -05:00
Merge 254bc81260 into 7f17759bfe
This commit is contained in:
commit
964f2da0f7
4 changed files with 107 additions and 9 deletions
3
changelogs/fragments/apt_repository.yml
Normal file
3
changelogs/fragments/apt_repository.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
bugfixes:
|
||||
- apt_repository - validate the line in sources.list (https://github.com/ansible/ansible/issues/85715).
|
||||
|
|
@ -282,6 +282,59 @@ class SourcesList(object):
|
|||
|
||||
return '%s.list' % _cleanup_filename(' '.join(parts[:1]))
|
||||
|
||||
@staticmethod
|
||||
def _validate_source(source: str) -> bool:
|
||||
"""
|
||||
Validate a source string according to the SOURCES.LIST(5).
|
||||
See: https://manpages.debian.org/trixie/apt/sources.list.5.en.html#ONE-LINE-STYLE_FORMAT
|
||||
"""
|
||||
parts = source.split()
|
||||
|
||||
if not parts:
|
||||
return False
|
||||
|
||||
# Extract the type and handle options
|
||||
entry_type = parts[0]
|
||||
if entry_type not in VALID_SOURCE_TYPES:
|
||||
return False
|
||||
|
||||
# Check for options enclosed in square brackets
|
||||
# The first element after the type might be the start of options
|
||||
if len(parts) > 1 and parts[1].startswith('['):
|
||||
if parts[1].endswith(']'):
|
||||
# For single-word options
|
||||
remaining_parts = parts[2:]
|
||||
else:
|
||||
# For multi-word options
|
||||
end_bracket_index = -1
|
||||
for i, part in enumerate(parts[2:], start=2):
|
||||
if part.endswith(']'):
|
||||
end_bracket_index = i
|
||||
break
|
||||
|
||||
if end_bracket_index != -1:
|
||||
remaining_parts = parts[end_bracket_index + 1:]
|
||||
else:
|
||||
# Malformed options, treat the whole thing as a single part for now.
|
||||
remaining_parts = parts[1:]
|
||||
return False
|
||||
else:
|
||||
remaining_parts = parts[1:]
|
||||
|
||||
# According to `sources.list(5)` man pages, only four fields are mandatory:
|
||||
# * `Types` either `deb` or/and `deb-src`
|
||||
# * `URIs` to repositories holding valid APT structure (unclear if multiple are allowed)
|
||||
# * `Suites` usually being distribution codenames
|
||||
# * `Component` most of the time `main`, but it's a section of the repository
|
||||
if remaining_parts[1].endswith('/') and len(remaining_parts) > 2:
|
||||
# Suites with trailing slash makes component optional
|
||||
return False
|
||||
if not remaining_parts[1].endswith('/') and len(remaining_parts) < 3:
|
||||
# Invalid line format
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _parse(self, line, raise_if_invalid_or_disabled=False):
|
||||
valid = False
|
||||
enabled = True
|
||||
|
|
@ -303,10 +356,7 @@ class SourcesList(object):
|
|||
# Duplicated whitespaces in a valid source spec will be removed.
|
||||
source = line.strip()
|
||||
if source:
|
||||
chunks = source.split()
|
||||
if chunks[0] in VALID_SOURCE_TYPES:
|
||||
valid = True
|
||||
source = ' '.join(chunks)
|
||||
valid = self._validate_source(source)
|
||||
|
||||
if raise_if_invalid_or_disabled and (not valid or not enabled):
|
||||
raise InvalidSource(line)
|
||||
|
|
@ -315,11 +365,11 @@ class SourcesList(object):
|
|||
|
||||
def load(self, file):
|
||||
group = []
|
||||
f = open(file, 'r')
|
||||
for n, line in enumerate(f):
|
||||
valid, enabled, source, comment = self._parse(line)
|
||||
group.append((n, valid, enabled, source, comment))
|
||||
self.files[file] = group
|
||||
with open(file, 'r') as f:
|
||||
for n, line in enumerate(f):
|
||||
valid, enabled, source, comment = self._parse(line)
|
||||
group.append((n, valid, enabled, source, comment))
|
||||
self.files[file] = group
|
||||
|
||||
def save(self):
|
||||
for filename, sources in list(self.files.items()):
|
||||
|
|
|
|||
|
|
@ -354,6 +354,20 @@
|
|||
- name: uninstall local-apt-repository with apt
|
||||
apt: pkg=local-apt-repository state=absent purge=yes
|
||||
|
||||
# Invalid repo with no component
|
||||
- name: Add repo with empty component
|
||||
ansible.builtin.apt_repository:
|
||||
repo: "deb http://archive.canonical.com/ubuntu jammy"
|
||||
state: present
|
||||
register: emptycomp_result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Assert that the repo was not added
|
||||
assert:
|
||||
that:
|
||||
- emptycomp_result is failed
|
||||
- "'Invalid repository string' in emptycomp_result.msg"
|
||||
|
||||
#
|
||||
# TEST: PPA HTTPS URL
|
||||
#
|
||||
|
|
|
|||
31
test/units/modules/test_apt_repository.py
Normal file
31
test/units/modules/test_apt_repository.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright: Contributors to the Ansible project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from ansible.modules.apt_repository import SourcesList
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line, expected", [
|
||||
pytest.param("deb http://deb.debian.org/debian stable main contrib non-free", True, id="valid_line"),
|
||||
pytest.param("# This is a commented line that should be ignored", False, id="commented_line"),
|
||||
pytest.param("deb http://ftp.us.debian.org/debian sid main", True, id="no_options_line"),
|
||||
pytest.param("deb-src http://ftp.debian.org/debian/ experimental/", True, id="suite_with_slash"),
|
||||
pytest.param("deb-src http://ftp.debian.org/debian/ experimental/ main", False, id="suite_with_slash_and_component"),
|
||||
pytest.param("deb [arch=amd64,i386] http://ftp.us.debian.org/debian sid main", True, id="multi_arch_option_line"),
|
||||
pytest.param("deb [trusted=yes arch=amd64] https://example.com/debian focal", False, id="invalid_line"),
|
||||
pytest.param("deb [trusted=yes arch=amd64] https://example.com/debian focal main", True, id="trusted_option_line"),
|
||||
pytest.param("deb [trusted=yes signed-by=/etc/apt/key.gpg] http://my.repo.com/ubuntu focal-updates main", True, id="signed_by_option_line"),
|
||||
pytest.param("deb-src [arch=amd64 trusted=yes] http://my.repo.com/ubuntu focal main universe", True, id="multiple_components_line"),
|
||||
pytest.param("deb [arch=amd64,i386 trusted=yes] http://my.repo.com/ubuntu focal main", True, id="multiple_arch_trusted_option_line"),
|
||||
pytest.param(
|
||||
"deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted # a comment at the end",
|
||||
True,
|
||||
id="comment_at_end_line"
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_validate(line, expected):
|
||||
assert SourcesList._validate_source(line) == expected
|
||||
Loading…
Reference in a new issue