From 75af50ddb2c830f83b5af15fb3c1079629c0a327 Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Sat, 14 Mar 2026 20:12:09 -0400 Subject: [PATCH] [pkg] add nfpm package generation - add nfpm_package bazel helper - add nfpm config generator for discovering executables and systemd units added as bazel deps - add packages for: - apcupsd-exporter - http-proxy - ldap-health-exporter - prometheus-helpers - runtime-utils --- bazel/pkg.bzl | 70 +++++ pkg/BUILD.bazel | 58 ++++ pkg/apcupsd-exporter.yaml | 15 ++ pkg/http-proxy.yaml | 15 ++ pkg/ldap-health-exporter.yaml | 15 ++ pkg/nfpmgen/BUILD.bazel | 24 ++ pkg/nfpmgen/config_generator.go | 249 ++++++++++++++++++ pkg/nfpmgen/main.go | 89 +++++++ pkg/prometheus-helpers.yaml | 15 ++ pkg/runtime-utils.yaml | 18 ++ ...1-Never-return-nil-from-getBuildInfo.patch | 25 ++ 11 files changed, 593 insertions(+) create mode 100644 bazel/pkg.bzl create mode 100644 pkg/BUILD.bazel create mode 100644 pkg/apcupsd-exporter.yaml create mode 100644 pkg/http-proxy.yaml create mode 100644 pkg/ldap-health-exporter.yaml create mode 100644 pkg/nfpmgen/BUILD.bazel create mode 100644 pkg/nfpmgen/config_generator.go create mode 100644 pkg/nfpmgen/main.go create mode 100644 pkg/prometheus-helpers.yaml create mode 100644 pkg/runtime-utils.yaml create mode 100644 thirdparty/go-version/0001-Never-return-nil-from-getBuildInfo.patch diff --git a/bazel/pkg.bzl b/bazel/pkg.bzl new file mode 100644 index 0000000..5144701 --- /dev/null +++ b/bazel/pkg.bzl @@ -0,0 +1,70 @@ +def _nfpm_package( + package_name, + config, + data, + packager, +): + extension_map = { + "archlinux": "pkg.tar.zst", + } + distro_vars = { + "archlinux": { + "ENV_DIR": "/etc/conf.d", + }, + "deb": { + "ENV_DIR": "/etc/default", + }, + "rpm": { + "ENV_DIR": "/etc/default", + }, + } + pkg_prefix = "{}_{}".format(package_name.replace("-", "_"), packager) + extra_args = [] + for target in data: + extra_args.append('--include="$(location {})"'.format(target)) + + for var, val in distro_vars.get(packager, {}).items(): + extra_args.append('--var="{}={}"'.format(var, val)) + + native.genrule( + name = "{}_nfpm_config".format(pkg_prefix), + tools = [ + "//pkg/nfpmgen", + ], + srcs = [config] + data, + outs = [ + "{}_nfpm_config.yaml".format(pkg_prefix), + ], + cmd_bash = """ + $(location //pkg/nfpmgen) --in="$(location {})" --out="$@" {} + """.format(config, " ".join(extra_args)), + ) + + native.genrule( + name = "{}_nfpm_pkg".format(pkg_prefix), + tools = [ + "@com_github_goreleaser_nfpm_v2//cmd/nfpm:nfpm", + ], + srcs = [ + ":{}_nfpm_config".format(pkg_prefix), + ] + data, + outs = [ + "{}.{}".format(package_name, extension_map.get(packager, packager)), + ], + cmd = " ".join([ + "$(location @com_github_goreleaser_nfpm_v2//cmd/nfpm:nfpm)", + "package", + "-p {}".format(packager), + "-f $(location :{}_nfpm_config)".format(pkg_prefix), + "-t $@", + ]), + ) + +def nfpm_package( + name, # type: str + config, # type: str + data, # type: list[str] +): + #for packager in ["deb", "archlinux", "rpm"]: + for packager in ["archlinux"]: + _nfpm_package(name, config, data, packager) \ No newline at end of file diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel new file mode 100644 index 0000000..a171e56 --- /dev/null +++ b/pkg/BUILD.bazel @@ -0,0 +1,58 @@ +load("//bazel:pkg.bzl", "nfpm_package") + +nfpm_package( + name = "runtime-utils", + config = ":runtime-utils.yaml", + data = [ + "//cmd/ephs_client", + "//cmd/metricbus_server", + "//cmd/metricbus_server:metric_collector_systemd_service", + "//cmd/mtls_exporter", + "//cmd/mtls_exporter:mtls_exporter_systemd_service", + "//cmd/mtls_supervisor", + "//cmd/sd_health_exporter", + "//cmd/sd_health_exporter:sd_health_exporter_systemd_service", + "//cmd/sd_publish", + "//cmd/sd_register", + "//cmd/sd_register:sd_register@_systemd_service", + "//cmd/sd_register:sd_register_systemd_service", + "//cmd/sd_watcher", + ], +) + +nfpm_package( + name = "apcupsd-exporter", + config = ":apcupsd-exporter.yaml", + data = [ + "//cmd/apcups_exporter", + "//cmd/apcups_exporter:apcupsd_exporter@_systemd_service", + "//cmd/apcups_exporter:apcupsd_exporter_systemd_service", + ], +) + +nfpm_package( + name = "http-proxy", + config = ":http-proxy.yaml", + data = [ + "//cmd/http_proxy", + "//cmd/http_proxy:http_proxy@_systemd_service", + ], +) + +nfpm_package( + name = "ldap-health-exporter", + config = ":ldap-health-exporter.yaml", + data = [ + "//cmd/ldap_health_exporter", + "//cmd/ldap_health_exporter:ldap_health_exporter@_systemd_service", + ], +) + +nfpm_package( + name = "prometheus-helpers", + config = ":prometheus-helpers.yaml", + data = [ + "//cmd/prometheus_http_discovery", + "//cmd/prometheus_http_discovery:prometheus_http_discovery_systemd_service", + ], +) diff --git a/pkg/apcupsd-exporter.yaml b/pkg/apcupsd-exporter.yaml new file mode 100644 index 0000000..b6b620c --- /dev/null +++ b/pkg/apcupsd-exporter.yaml @@ -0,0 +1,15 @@ +name: "${const:ExePrefix}apcupsd-exporter" +arch: "amd64" +platform: "linux" +version: "${sub:${const:Version},+unset,}" +section: "net" +priority: "extra" +maintainer: "Bazel Build Service " +depends: + - ${const:ExePrefix}runtime-utils +description: | + no description provided +vendor: "${const:OrgName}" +homepage: "https://go.fuhry.dev/runtime" +license: "Proprietary" +contents: [] \ No newline at end of file diff --git a/pkg/http-proxy.yaml b/pkg/http-proxy.yaml new file mode 100644 index 0000000..3b83f6f --- /dev/null +++ b/pkg/http-proxy.yaml @@ -0,0 +1,15 @@ +name: "${const:ExePrefix}http-proxy" +arch: "amd64" +platform: "linux" +version: "${sub:${const:Version},+unset,}" +section: "net" +priority: "extra" +maintainer: "Bazel Build Service " +depends: + - ${const:ExePrefix}runtime-utils +description: | + no description provided +vendor: "${const:OrgName}" +homepage: "https://go.fuhry.dev/runtime" +license: "Proprietary" +contents: [] \ No newline at end of file diff --git a/pkg/ldap-health-exporter.yaml b/pkg/ldap-health-exporter.yaml new file mode 100644 index 0000000..e7013d2 --- /dev/null +++ b/pkg/ldap-health-exporter.yaml @@ -0,0 +1,15 @@ +name: "${const:ExePrefix}ldap-health-exporter" +arch: "amd64" +platform: "linux" +version: "${sub:${const:Version},+unset,}" +section: "net" +priority: "extra" +maintainer: "Bazel Build Service " +depends: + - ${const:ExePrefix}runtime-utils +description: | + no description provided +vendor: "${const:OrgName}" +homepage: "https://go.fuhry.dev/runtime" +license: "Proprietary" +contents: [] \ No newline at end of file diff --git a/pkg/nfpmgen/BUILD.bazel b/pkg/nfpmgen/BUILD.bazel new file mode 100644 index 0000000..6260654 --- /dev/null +++ b/pkg/nfpmgen/BUILD.bazel @@ -0,0 +1,24 @@ +load("@rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "nfpmgen_lib", + srcs = [ + "config_generator.go", + "main.go", + ], + importpath = "go.fuhry.dev/runtime/pkg/nfpmgen", + visibility = ["//visibility:private"], + deps = [ + "//constants", + "//utils/subst", + "@com_github_goreleaser_nfpm_v2//:nfpm", + "@com_github_goreleaser_nfpm_v2//files", + "@in_gopkg_yaml_v3//:yaml_v3", + ], +) + +go_binary( + name = "nfpmgen", + embed = [":nfpmgen_lib"], + visibility = ["//visibility:public"], +) diff --git a/pkg/nfpmgen/config_generator.go b/pkg/nfpmgen/config_generator.go new file mode 100644 index 0000000..d91fc64 --- /dev/null +++ b/pkg/nfpmgen/config_generator.go @@ -0,0 +1,249 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path" + "regexp" + "strings" + + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + "gopkg.in/yaml.v3" + + "go.fuhry.dev/runtime/constants" + "go.fuhry.dev/runtime/utils/subst" +) + +var verbose bool + +func debug(fmtstr string, args ...any) { + if !verbose { + return + } + + fmt.Fprintf(os.Stderr, fmtstr+"\n", args...) +} + +type NfpmConfig struct { + *nfpm.Config + + Vars subst.KV `yaml:"vars" json:"vars" jsonschema:"title=map of context vars to inject into subst"` + + files map[string]struct{} `yaml:"-" json:"-"` +} + +type executable string + +var systemdUnitFilename = regexp.MustCompile(`\.(service|target|socket|preset)$`) + +func (e executable) String() string { + return string(e) +} + +func (e executable) buildPath(bazelBinDir string) string { + return path.Join( + bazelBinDir, "cmd", e.String(), e.String()+"_", e.String()) +} + +func (e executable) pkgPath(vars subst.KV) string { + exeDir, ok := vars["EXEDIR"] + if !ok { + exeDir = "/usr/bin" + } + + exeName := constants.ExePrefix + strings.ReplaceAll(string(e), "_", "-") + + return path.Join(exeDir, exeName) +} + +func LoadNfpmConfig(yamlPath string, fileAllowlist []string, varOverrides subst.KV) (*NfpmConfig, error) { + contents, err := os.ReadFile(yamlPath) + if err != nil { + return nil, err + } + + // first load the "vars" key to substitute throughout the rest of the file + out := &NfpmConfig{ + Config: new(nfpm.Config), + Vars: make(subst.KV), + + files: make(map[string]struct{}), + } + + for _, f := range fileAllowlist { + out.files[f] = struct{}{} + } + + if err := yaml.Unmarshal(contents, out); err != nil { + return nil, err + } + + for k, v := range varOverrides { + debug("var %q overridden to %q via command line", k, v) + out.Vars[k] = v + } + + substContents, err := subst.Eval(out.Vars, string(contents)) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal([]byte(substContents), out.Config); err != nil { + return nil, err + } + + return out, nil +} + +func (c *NfpmConfig) DiscoverBinaries(bazelBinDir string) error { + wd, err := os.Getwd() + if err != nil { + return err + } + + cmdDir, err := os.ReadDir(path.Join(wd, bazelBinDir, "cmd")) + if err != nil { + return err + } + + for _, entry := range cmdDir { + if entry.Name() == "." || entry.Name() == ".." || !entry.IsDir() { + continue + } + + exe := executable(entry.Name()) + + exePath := exe.buildPath(bazelBinDir) + + if _, ok := c.files[exePath]; !ok { + debug("exe not allowlisted in includes: %s", exePath) + continue + } + + debug("evaluating exe with buildPath %s", exePath) + + if derefPath, err := os.Readlink(exePath); err == nil { + if path.IsAbs(derefPath) { + exePath = derefPath + } else { + exePath = path.Join( + path.Dir(exePath), + derefPath, + ) + } + + debug(" -> dereferenced symlink to: %s", exePath) + } + + st, err := os.Stat(exePath) + if err != nil { + debug(" -> executable not found") + continue + } + + if st.Mode()&os.FileMode(0o500) != 0o500 || !st.Mode().IsRegular() { + debug(" -> not a regular file or not executable") + continue + } + + c.Config.Contents = append(c.Config.Contents, &files.Content{ + Source: exePath, + Destination: exe.pkgPath(c.Vars), + FileInfo: &files.ContentFileInfo{ + Owner: "root", + Group: "root", + Mode: os.FileMode(0o755), + }, + }) + } + + return nil +} + +func recursiveDirectoryScan(baseDir string, match func(e os.DirEntry) bool) ([]string, error) { + var out []string + entries, err := os.ReadDir(baseDir) + if err != nil { + return nil, err + } + + for _, e := range entries { + abs := path.Join(baseDir, e.Name()) + if match(e) { + out = append(out, abs) + } + + if e.IsDir() { + children, err := recursiveDirectoryScan(abs, match) + if err != nil { + return nil, err + } + out = append(out, children...) + } + } + + return out, nil +} + +func (c *NfpmConfig) DiscoverSystemdUnits(bazelBinDir string) error { + unitFiles, err := recursiveDirectoryScan( + bazelBinDir, + func(e os.DirEntry) bool { + if e.IsDir() { + return false + } + + return systemdUnitFilename.MatchString(e.Name()) + }, + ) + if err != nil { + return err + } + + for _, f := range unitFiles { + if _, ok := c.files[f]; !ok { + debug("unit file not allowlisted in includes: %s", f) + continue + } + + if derefPath, err := os.Readlink(f); err == nil { + if path.IsAbs(derefPath) { + f = derefPath + } else { + f = path.Join( + path.Dir(f), + derefPath, + ) + } + } + + destName := constants.ExePrefix + path.Base(f) + + c.Config.Contents = append(c.Config.Contents, &files.Content{ + Source: f, + Destination: "/usr/lib/systemd/" + destName, + FileInfo: &files.ContentFileInfo{ + Owner: "root", + Group: "root", + Mode: os.FileMode(0o644), + }, + }) + } + + return nil +} + +func (c *NfpmConfig) Generate() (string, error) { + out, err := yaml.Marshal(c.Config) + if err != nil { + return "", err + } + + return string(out), nil +} + +func init() { + flag.BoolVar(&verbose, "verbose", false, "enable debug logging") +} diff --git a/pkg/nfpmgen/main.go b/pkg/nfpmgen/main.go new file mode 100644 index 0000000..6fa7be7 --- /dev/null +++ b/pkg/nfpmgen/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "strings" + + "go.fuhry.dev/runtime/utils/subst" +) + +func main() { + yamlFile := flag.String("in", "", "path to yaml template to consume") + outFile := flag.String("out", "", "output file to write; if omitted, uses stdout") + + var includes []string + parseInclude := func(flagVal string) error { + _, err := os.Lstat(flagVal) + if err != nil { + return fmt.Errorf("error processing include %q: %v", flagVal, err) + } + includes = append(includes, flagVal) + + return nil + } + flag.Func("include", "file to consider for package", parseInclude) + + kv := make(subst.KV) + addKv := func(flagVal string) error { + k, v, ok := strings.Cut(flagVal, "=") + if !ok { + return errors.New("-var= usage: var=value, e.g. -var=FOO=BAR") + } + kv[k] = v + return nil + } + flag.Func("var", "extra variable to set", addKv) + + flag.Parse() + + c, err := LoadNfpmConfig(*yamlFile, includes, kv) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + bazelBinDir := "" + if outFile != nil && *outFile != "" { + // pushd to the bazel-bin dir `bazel-bin/[arch]/bin` directory + parts := strings.Split(*outFile, "/") + if len(parts) >= 3 { + bazelBinDir = strings.Join(parts[:3], "/") + } + } + + debug("loaded nfpm config template: %s\ncwd: %s\nout: %s\n", + *yamlFile, wd, *outFile) + + if err := c.DiscoverBinaries(bazelBinDir); err != nil { + panic(err) + } + + if err := c.DiscoverSystemdUnits(bazelBinDir); err != nil { + panic(err) + } + + out, err := c.Generate() + if err != nil { + panic(err) + } + + err = os.Chdir(wd) + if err != nil { + panic(err) + } + + if outFile == nil || *outFile == "" { + fmt.Println(out) + } else { + if err := os.WriteFile(*outFile, []byte(out), os.FileMode(0o644)); err != nil { + panic(err) + } + } +} diff --git a/pkg/prometheus-helpers.yaml b/pkg/prometheus-helpers.yaml new file mode 100644 index 0000000..de239c7 --- /dev/null +++ b/pkg/prometheus-helpers.yaml @@ -0,0 +1,15 @@ +name: "${const:ExePrefix}prometheus-helpers" +arch: "amd64" +platform: "linux" +version: "${sub:${const:Version},+unset,}" +section: "net" +priority: "extra" +maintainer: "Bazel Build Service " +depends: + - ${const:ExePrefix}runtime-utils +description: | + no description provided +vendor: "${const:OrgName}" +homepage: "https://go.fuhry.dev/runtime" +license: "Proprietary" +contents: [] \ No newline at end of file diff --git a/pkg/runtime-utils.yaml b/pkg/runtime-utils.yaml new file mode 100644 index 0000000..6e87438 --- /dev/null +++ b/pkg/runtime-utils.yaml @@ -0,0 +1,18 @@ +name: "${const:ExePrefix}runtime-utils" +arch: "amd64" +platform: "linux" +version: "${sub:${const:Version},+unset,}" +section: "net" +priority: "extra" +replaces: + - ${const:OrgSlug}-service-discovery +depends: + - glibc +conflicts: + - ${const:OrgSlug}-service-discovery +maintainer: "Bazel Build Service " +description: Tools needed by virtually all ${const:OrgName} systems for service discovery and metrics +vendor: "${const:OrgName}" +homepage: "https://go.fuhry.dev/runtime" +license: "Proprietary" +contents: [] \ No newline at end of file diff --git a/thirdparty/go-version/0001-Never-return-nil-from-getBuildInfo.patch b/thirdparty/go-version/0001-Never-return-nil-from-getBuildInfo.patch new file mode 100644 index 0000000..eaf5f63 --- /dev/null +++ b/thirdparty/go-version/0001-Never-return-nil-from-getBuildInfo.patch @@ -0,0 +1,25 @@ +From 0edcc9ab033623894d59b9f301a45dd95b322ddd Mon Sep 17 00:00:00 2001 +From: Dan Fuhry +Date: Tue, 20 Jan 2026 21:39:00 -0500 +Subject: [PATCH] Never return nil from getBuildInfo + +--- + version.go | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/version.go b/version.go +index 5734d52..2a153e7 100644 +--- a/version.go ++++ b/version.go +@@ -36,7 +36,7 @@ type Info struct { + func getBuildInfo() *debug.BuildInfo { + bi, ok := debug.ReadBuildInfo() + if !ok { +- return nil ++ return &debug.BuildInfo{} + } + return bi + } +-- +2.52.0 + -- 2.52.0