a73x

internal/site/manifest.go

Ref:   Size: 3.1 KiB   History

package site

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"regexp"

	"github.com/a73x/eitri/internal/server/release"
)

// bundlePat matches the host bundles the release stage drops in dist/: a Linux
// host takes eitri-server_<v>_linux_<arch>.tar.gz (server, agent and their
// units), a Mac eitri-agent_<v>_darwin_<arch>.tar.gz (agent and its installer).
// Each carries an eitri-agent member, so the tarball an operator unpacks by
// hand is the same artifact a self-updating agent downloads — it verifies the
// sha and extracts that member. The client and issuer bundles carry no agent
// and are deliberately outside the pattern.
var bundlePat = regexp.MustCompile(`^eitri-(?:server|agent)_[^_]+_([a-z0-9]+)_([a-z0-9]+)\.tar\.gz$`)

// chPat matches the pinned cloud-hypervisor binaries mirrored into dist/:
// cloud-hypervisor_<os>_<arch>. What the agent bootstraps its runtime from.
var chPat = regexp.MustCompile(`^cloud-hypervisor_([a-z0-9]+)_([a-z0-9]+)$`)

// firmwareName is the guest UEFI firmware mirrored into dist/, if present.
// edk2 CLOUDHV is x86-64 only, so it manifests under a single platform key.
const firmwareName = "CLOUDHV.fd"

// BuildManifest scans distDir for host bundles — required — plus optional
// runtime artifacts (a pinned cloud-hypervisor and guest firmware) and
// produces the release manifest the server polls and the agent bootstraps
// from. Sharing release.Manifest with the consumers is deliberate: the wire
// contract lives in one type.
func BuildManifest(version, distDir, baseURL string) (release.Manifest, error) {
	if version == "" {
		return release.Manifest{}, fmt.Errorf("version required")
	}
	m := release.Manifest{
		Version:   version,
		Artifacts: map[string]map[string]release.Artifact{"eitri-agent": {}},
	}
	entries, err := os.ReadDir(distDir)
	if err != nil {
		return release.Manifest{}, err
	}
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		key, platform := "", ""
		switch {
		case bundlePat.MatchString(e.Name()):
			match := bundlePat.FindStringSubmatch(e.Name())
			key, platform = "eitri-agent", match[1]+"/"+match[2]
		case chPat.MatchString(e.Name()):
			match := chPat.FindStringSubmatch(e.Name())
			key, platform = "cloud-hypervisor", match[1]+"/"+match[2]
		case e.Name() == firmwareName:
			key, platform = "firmware", "linux/amd64"
		default:
			continue
		}
		sum, err := fileSHA256(filepath.Join(distDir, e.Name()))
		if err != nil {
			return release.Manifest{}, err
		}
		u, err := url.JoinPath(baseURL, e.Name())
		if err != nil {
			return release.Manifest{}, err
		}
		if m.Artifacts[key] == nil {
			m.Artifacts[key] = map[string]release.Artifact{}
		}
		m.Artifacts[key][platform] = release.Artifact{
			URL:    u,
			SHA256: sum,
		}
	}
	if len(m.Artifacts["eitri-agent"]) == 0 {
		return release.Manifest{}, fmt.Errorf("no host bundles in %s", distDir)
	}
	return m, nil
}

func fileSHA256(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}