diff options
Diffstat (limited to 'packaging/dag/nd.py')
-rw-r--r-- | packaging/dag/nd.py | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/packaging/dag/nd.py b/packaging/dag/nd.py new file mode 100644 index 00000000..d59adf30 --- /dev/null +++ b/packaging/dag/nd.py @@ -0,0 +1,409 @@ +from typing import List + +import enum +import os +import pathlib +import uuid + +import dagger +import jinja2 + +import imageutils + + +class Platform: + def __init__(self, platform: str): + self.platform = dagger.Platform(platform) + + def escaped(self) -> str: + return str(self.platform).removeprefix("linux/").replace("/", "_") + + def __eq__(self, other): + if isinstance(other, Platform): + return self.platform == other.platform + elif isinstance(other, dagger.Platform): + return self.platform == other + else: + return NotImplemented + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(self.platform) + + def __str__(self) -> str: + return str(self.platform) + + +SUPPORTED_PLATFORMS = set( + [ + Platform("linux/x86_64"), + Platform("linux/arm64"), + Platform("linux/i386"), + Platform("linux/arm/v7"), + Platform("linux/arm/v6"), + Platform("linux/ppc64le"), + Platform("linux/s390x"), + Platform("linux/riscv64"), + ] +) + + +SUPPORTED_DISTRIBUTIONS = set( + [ + "alpine_3_18", + "alpine_3_19", + "amazonlinux2", + "centos7", + "centos-stream8", + "centos-stream9", + "debian10", + "debian11", + "debian12", + "fedora37", + "fedora38", + "fedora39", + "opensuse15.4", + "opensuse15.5", + "opensusetumbleweed", + "oraclelinux8", + "oraclelinux9", + "rockylinux8", + "rockylinux9", + "ubuntu20.04", + "ubuntu22.04", + "ubuntu23.04", + "ubuntu23.10", + ] +) + + +class Distribution: + def __init__(self, display_name): + self.display_name = display_name + + if self.display_name == "alpine_3_18": + self.docker_tag = "alpine:3.18" + self.builder = imageutils.build_alpine_3_18 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "alpine_3_19": + self.docker_tag = "alpine:3.19" + self.builder = imageutils.build_alpine_3_19 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "amazonlinux2": + self.docker_tag = "amazonlinux:2" + self.builder = imageutils.build_amazon_linux_2 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "centos7": + self.docker_tag = "centos:7" + self.builder = imageutils.build_centos_7 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "centos-stream8": + self.docker_tag = "quay.io/centos/centos:stream8" + self.builder = imageutils.build_centos_stream_8 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "centos-stream9": + self.docker_tag = "quay.io/centos/centos:stream9" + self.builder = imageutils.build_centos_stream_9 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "debian10": + self.docker_tag = "debian:10" + self.builder = imageutils.build_debian_10 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "debian11": + self.docker_tag = "debian:11" + self.builder = imageutils.build_debian_11 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "debian12": + self.docker_tag = "debian:12" + self.builder = imageutils.build_debian_12 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "fedora37": + self.docker_tag = "fedora:37" + self.builder = imageutils.build_fedora_37 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "fedora38": + self.docker_tag = "fedora:38" + self.builder = imageutils.build_fedora_38 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "fedora39": + self.docker_tag = "fedora:39" + self.platforms = SUPPORTED_PLATFORMS + self.builder = imageutils.build_fedora_39 + elif self.display_name == "opensuse15.4": + self.docker_tag = "opensuse/leap:15.4" + self.builder = imageutils.build_opensuse_15_4 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "opensuse15.5": + self.docker_tag = "opensuse/leap:15.5" + self.builder = imageutils.build_opensuse_15_5 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "opensusetumbleweed": + self.docker_tag = "opensuse/tumbleweed:latest" + self.builder = imageutils.build_opensuse_tumbleweed + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "oraclelinux8": + self.docker_tag = "oraclelinux:8" + self.builder = imageutils.build_oracle_linux_8 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "oraclelinux9": + self.docker_tag = "oraclelinux:9" + self.builder = imageutils.build_oracle_linux_9 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "rockylinux8": + self.docker_tag = "rockylinux:8" + self.builder = imageutils.build_rocky_linux_8 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "rockylinux9": + self.docker_tag = "rockylinux:9" + self.builder = imageutils.build_rocky_linux_9 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "ubuntu20.04": + self.docker_tag = "ubuntu:20.04" + self.builder = imageutils.build_ubuntu_20_04 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "ubuntu22.04": + self.docker_tag = "ubuntu:22.04" + self.builder = imageutils.build_ubuntu_22_04 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "ubuntu23.04": + self.docker_tag = "ubuntu:23.04" + self.builder = imageutils.build_ubuntu_23_04 + self.platforms = SUPPORTED_PLATFORMS + elif self.display_name == "ubuntu23.10": + self.docker_tag = "ubuntu:23.10" + self.builder = imageutils.build_ubuntu_23_10 + self.platforms = SUPPORTED_PLATFORMS + else: + raise ValueError(f"Unknown distribution: {self.display_name}") + + def _cache_volume( + self, client: dagger.Client, platform: dagger.Platform, path: str + ) -> dagger.CacheVolume: + tag = "_".join([self.display_name, Platform(platform).escaped()]) + return client.cache_volume(f"{path}-{tag}") + + def build( + self, client: dagger.Client, platform: dagger.Platform + ) -> dagger.Container: + if platform not in self.platforms: + raise ValueError( + f"Building {self.display_name} is not supported on {platform}." + ) + + ctr = self.builder(client, platform) + ctr = imageutils.install_cargo(ctr) + + return ctr + + +class FeatureFlags(enum.Flag): + DBEngine = enum.auto() + GoPlugin = enum.auto() + ExtendedBPF = enum.auto() + LogsManagement = enum.auto() + MachineLearning = enum.auto() + BundledProtobuf = enum.auto() + + +class NetdataInstaller: + def __init__( + self, + platform: Platform, + distro: Distribution, + repo_root: pathlib.Path, + prefix: pathlib.Path, + features: FeatureFlags, + ): + self.platform = platform + self.distro = distro + self.repo_root = repo_root + self.prefix = prefix + self.features = features + + def _mount_repo( + self, client: dagger.Client, ctr: dagger.Container, repo_root: pathlib.Path + ) -> dagger.Container: + host_repo_root = pathlib.Path(__file__).parent.parent.parent.as_posix() + exclude_dirs = ["build", "fluent-bit/build", "packaging/dag"] + + # The installer builds/stores intermediate artifacts under externaldeps/ + # We add a volume to speed up rebuilds. The volume has to be unique + # per platform/distro in order to avoid mixing unrelated artifacts + # together. + externaldeps = self.distro._cache_volume(client, self.platform, "externaldeps") + + ctr = ( + ctr.with_directory( + self.repo_root.as_posix(), client.host().directory(host_repo_root) + ) + .with_workdir(self.repo_root.as_posix()) + .with_mounted_cache( + os.path.join(self.repo_root, "externaldeps"), externaldeps + ) + ) + + return ctr + + def install(self, client: dagger.Client, ctr: dagger.Container) -> dagger.Container: + args = ["--dont-wait", "--dont-start-it", "--disable-telemetry"] + + if FeatureFlags.DBEngine not in self.features: + args.append("--disable-dbengine") + + if FeatureFlags.GoPlugin not in self.features: + args.append("--disable-go") + + if FeatureFlags.ExtendedBPF not in self.features: + args.append("--disable-ebpf") + + if FeatureFlags.LogsManagement not in self.features: + args.append("--disable-logsmanagement") + + if FeatureFlags.MachineLearning not in self.features: + args.append("--disable-ml") + + if FeatureFlags.BundledProtobuf not in self.features: + args.append("--use-system-protobuf") + + args.extend(["--install-prefix", self.prefix.parent.as_posix()]) + + ctr = self._mount_repo(client, ctr, self.repo_root.as_posix()) + + ctr = ctr.with_env_variable( + "NETDATA_CMAKE_OPTIONS", "-DCMAKE_BUILD_TYPE=Debug" + ).with_exec(["./netdata-installer.sh"] + args) + + return ctr + + +class Endpoint: + def __init__(self, hostname: str, port: int): + self.hostname = hostname + self.port = port + + def __str__(self): + return ":".join([self.hostname, str(self.port)]) + + +class ChildStreamConf: + def __init__( + self, + installer: NetdataInstaller, + destinations: List[Endpoint], + api_key: uuid.UUID, + ): + self.installer = installer + self.substitutions = { + "enabled": "yes", + "destination": " ".join([str(dst) for dst in destinations]), + "api_key": api_key, + "timeout_seconds": 60, + "default_port": 19999, + "send_charts_matching": "*", + "buffer_size_bytes": 1024 * 1024, + "reconnect_delay_seconds": 5, + "initial_clock_resync_iterations": 60, + } + + def render(self) -> str: + tmpl_path = pathlib.Path(__file__).parent / "files/child_stream.conf" + with open(tmpl_path) as fp: + tmpl = jinja2.Template(fp.read()) + + return tmpl.render(**self.substitutions) + + +class ParentStreamConf: + def __init__(self, installer: NetdataInstaller, api_key: uuid.UUID): + self.installer = installer + self.substitutions = { + "api_key": str(api_key), + "enabled": "yes", + "allow_from": "*", + "default_history": 3600, + "health_enabled_by_default": "auto", + "default_postpone_alarms_on_connect_seconds": 60, + "multiple_connections": "allow", + } + + def render(self) -> str: + tmpl_path = pathlib.Path(__file__).parent / "files/parent_stream.conf" + with open(tmpl_path) as fp: + tmpl = jinja2.Template(fp.read()) + + return tmpl.render(**self.substitutions) + + +class StreamConf: + def __init__(self, child_conf: ChildStreamConf, parent_conf: ParentStreamConf): + self.child_conf = child_conf + self.parent_conf = parent_conf + + def render(self) -> str: + child_section = self.child_conf.render() if self.child_conf else "" + parent_section = self.parent_conf.render() if self.parent_conf else "" + return "\n".join([child_section, parent_section]) + + +class AgentContext: + def __init__( + self, + client: dagger.Client, + platform: dagger.Platform, + distro: Distribution, + installer: NetdataInstaller, + endpoint: Endpoint, + api_key: uuid.UUID, + allow_children: bool, + ): + self.client = client + self.platform = platform + self.distro = distro + self.installer = installer + self.endpoint = endpoint + self.api_key = api_key + self.allow_children = allow_children + + self.parent_contexts = [] + + self.built_distro = False + self.built_agent = False + + def add_parent(self, parent_context: "AgentContext"): + self.parent_contexts.append(parent_context) + + def build_container(self) -> dagger.Container: + ctr = self.distro.build(self.client, self.platform) + ctr = self.installer.install(self.client, ctr) + + if len(self.parent_contexts) == 0 and not self.allow_children: + return ctr.with_exposed_port(self.endpoint.port) + + destinations = [parent_ctx.endpoint for parent_ctx in self.parent_contexts] + child_stream_conf = ChildStreamConf(self.installer, destinations, self.api_key) + + parent_stream_conf = None + if self.allow_children: + parent_stream_conf = ParentStreamConf(self.installer, self.api_key) + + stream_conf = StreamConf(child_stream_conf, parent_stream_conf) + + # write the stream conf to localhost and cp it in the container + host_stream_conf_path = pathlib.Path( + f"/tmp/{self.endpoint.hostname}_stream.conf" + ) + with open(host_stream_conf_path, "w") as fp: + fp.write(stream_conf.render()) + + ctr_stream_conf_path = self.installer.prefix / "etc/netdata/stream.conf" + + ctr = ctr.with_file( + ctr_stream_conf_path.as_posix(), + self.client.host().file(host_stream_conf_path.as_posix()), + ) + + ctr = ctr.with_exposed_port(self.endpoint.port) + + return ctr |