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
}