diff --git a/src/bci_build/package/__init__.py b/src/bci_build/package/__init__.py
index 356d75124..f33750fb3 100644
--- a/src/bci_build/package/__init__.py
+++ b/src/bci_build/package/__init__.py
@@ -28,6 +28,7 @@
from bci_build.registry import ApplicationCollectionRegistry
from bci_build.registry import Registry
from bci_build.registry import publish_registry
+from bci_build.service import Service
from bci_build.templates import DOCKERFILE_TEMPLATE
from bci_build.templates import INFOHEADER_TEMPLATE
from bci_build.templates import KIWI_TEMPLATE
@@ -107,6 +108,21 @@ def __post_init__(self) -> None:
if self.file_name and "readme" in self.file_name.lower():
raise ValueError(f"Cannot replace variables in {self.file_name}!")
+ def to_service(self, default_file_name: str) -> Service:
+ """Convert this replacement into a
+ :py:class:`~bci__build.service.Service`.
+
+ """
+ return Service(
+ name="replace_using_package_version",
+ param=[
+ ("file", self.file_name or default_file_name),
+ ("regex", self.regex_in_build_description),
+ ("package", self.package_name),
+ ]
+ + ([("parse-version", self.parse_version)] if self.parse_version else []),
+ )
+
def _build_tag_prefix(os_version: OsVersion) -> str:
if os_version == OsVersion.TUMBLEWEED:
diff --git a/src/bci_build/service.py b/src/bci_build/service.py
new file mode 100644
index 000000000..24dea7348
--- /dev/null
+++ b/src/bci_build/service.py
@@ -0,0 +1,41 @@
+"""This module includes an abstraction over source services in the Open Build
+Service.
+
+"""
+
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from dataclasses import field
+from typing import Literal
+
+
+@dataclass(kw_only=True, frozen=True)
+class Service:
+ """Representation of an arbitrary source service in the Open Build Service."""
+
+ #: name of this service
+ name: str
+
+ #: unsorted list of parameters of this source service as a list of tuples
+ #: where the first value is the parameter's name and the second is the
+ #: parameter's value
+ param: list[tuple[str, str]] = field(default_factory=list)
+
+ #: service mode (i.e. when the service runs)
+ mode: Literal["buildtime"] = "buildtime"
+
+ def as_xml_element(self) -> ET.Element:
+ """Coverts this source service into a
+ :py:class:`~xml.etree.ElementTree.Element`.
+
+ """
+ root = ET.Element("service", attrib={"name": self.name, "mode": self.mode})
+
+ for param in self.param:
+ (p := ET.Element("param", attrib={"name": param[0]})).text = param[1]
+ root.append(p)
+
+ return root
+
+ def __str__(self) -> str:
+ return ET.tostring(self.as_xml_element()).decode("utf-8")
diff --git a/tests/test_service.py b/tests/test_service.py
index 10ab80d7f..7dd59d2c7 100644
--- a/tests/test_service.py
+++ b/tests/test_service.py
@@ -1,11 +1,69 @@
+import pytest
+
from bci_build.container_attributes import BuildType
from bci_build.containercrate import ContainerCrate
from bci_build.os_version import OsVersion
from bci_build.package import DevelopmentContainer
from bci_build.package import ParseVersion
from bci_build.package import Replacement
+from bci_build.service import Service
from bci_build.templates import SERVICE_TEMPLATE
+
+def test_service_without_params_as_xml():
+ assert """""" == str(Service(name="foo"))
+
+
+def test_service_with_params_as_xml():
+ assert (
+ """barfoo"""
+ == str(Service(name="foo", param=[("baz", "bar"), ("baz", "foo")]))
+ )
+
+
+@pytest.mark.parametrize(
+ "replacement, default_file_name, service",
+ [
+ # bare bone example
+ (
+ Replacement(regex := "%%ver%%", pkg := "pkgFoo"),
+ "Dockerfile",
+ Service(
+ name=(name := "replace_using_package_version"),
+ param=[("file", "Dockerfile"), ("regex", regex), ("package", pkg)],
+ ),
+ ),
+ # the default file name is ignored if the parameter file_name is given
+ (
+ Replacement(regex, pkg, file_name=(fname := "testfile")),
+ "Dockerfile",
+ Service(
+ name=name,
+ param=[("file", fname), ("regex", regex), ("package", pkg)],
+ ),
+ ),
+ # specify a parse_version
+ (
+ Replacement(regex, pkg, parse_version=ParseVersion.MAJOR),
+ "Dockerfile",
+ Service(
+ name=name,
+ param=[
+ ("file", "Dockerfile"),
+ ("regex", regex),
+ ("package", pkg),
+ ("parse-version", "major"),
+ ],
+ ),
+ ),
+ ],
+)
+def test_replacement_to_service(
+ replacement: Replacement, default_file_name: str, service: Service
+):
+ assert replacement.to_service(default_file_name) == service
+
+
_BASE_KWARGS = {
"name": "test",
"package_name": "test-image",