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