mirror of
https://github.com/juanfont/headscale.git
synced 2026-05-23 10:42:30 +09:00
cmd/vendorhash: track vendor SRI in flakehashes.json
Move the headscale vendorHash out of flake.nix into a content- addressed flakehashes.json maintained by a small Go tool. The schema and goModFingerprint algorithm mirror upstream tailscale's tool/updateflakes so a future shared library extraction is trivial. vendorhash check verifies flakehashes.json against the current go.mod/go.sum. Hot path is a sha256 over those two files, so re-runs without input change are essentially free; only an actual fingerprint drift triggers go mod vendor + nardump.SRI. vendorhash update recomputes both fields and rewrites the JSON. The nix-vendor-sri devShell shim now wraps it.
This commit is contained in:
221
cmd/vendorhash/main.go
Normal file
221
cmd/vendorhash/main.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// vendorhash maintains the Nix SRI hash for the Go module vendor tree
|
||||
// and stores it in flakehashes.json alongside a content fingerprint of
|
||||
// go.mod and go.sum.
|
||||
//
|
||||
// Each block records its input fingerprint (goModSum) so that re-runs
|
||||
// with no input change are essentially free: the fast path is just a
|
||||
// sha256 over two small files. The vendor tree is only re-walked when
|
||||
// the fingerprint actually drifts.
|
||||
//
|
||||
// Subcommands:
|
||||
//
|
||||
// vendorhash check exit non-zero if flakehashes.json is stale
|
||||
// vendorhash update recompute and rewrite flakehashes.json
|
||||
//
|
||||
// The JSON schema and goModFingerprint algorithm mirror upstream
|
||||
// tailscale's tool/updateflakes so a future shared library extraction
|
||||
// is straightforward.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"tailscale.com/cmd/nardump/nardump"
|
||||
)
|
||||
|
||||
const (
|
||||
hashesFile = "flakehashes.json"
|
||||
goModFile = "go.mod"
|
||||
goSumFile = "go.sum"
|
||||
)
|
||||
|
||||
type FlakeHashes struct {
|
||||
Vendor VendorBlock `json:"vendor"`
|
||||
}
|
||||
|
||||
type VendorBlock struct {
|
||||
GoModSum string `json:"goModSum"`
|
||||
SRI string `json:"sri"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var err error
|
||||
|
||||
switch os.Args[1] {
|
||||
case "check":
|
||||
err = cmdCheck(ctx)
|
||||
case "update":
|
||||
err = cmdUpdate(ctx)
|
||||
case "-h", "--help", "help":
|
||||
usage()
|
||||
return
|
||||
default:
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, errStale) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "vendorhash:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: vendorhash <check|update>")
|
||||
}
|
||||
|
||||
// errStale signals to main that the check found a mismatch; it has
|
||||
// already printed a remediation message, so main should exit 1
|
||||
// silently.
|
||||
var errStale = errors.New("vendor hash stale")
|
||||
|
||||
// cmdCheck verifies that flakehashes.json matches the current
|
||||
// go.mod/go.sum. The fast path (fingerprint unchanged) costs only
|
||||
// a sha256 over the two files. On mismatch, it computes the actual
|
||||
// SRI so the failure message gives the developer the value to paste
|
||||
// (or to run `vendorhash update`).
|
||||
func cmdCheck(ctx context.Context) error {
|
||||
hashes, err := loadHashes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
curFP, err := goModFingerprint()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if curFP == hashes.Vendor.GoModSum {
|
||||
return nil
|
||||
}
|
||||
|
||||
curSRI, err := hashVendor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "vendor hash is stale.")
|
||||
fmt.Fprintf(os.Stderr, " expected goModSum: %s\n", hashes.Vendor.GoModSum)
|
||||
fmt.Fprintf(os.Stderr, " actual goModSum: %s\n", curFP)
|
||||
fmt.Fprintf(os.Stderr, " expected sri: %s\n", hashes.Vendor.SRI)
|
||||
fmt.Fprintf(os.Stderr, " actual sri: %s\n", curSRI)
|
||||
fmt.Fprintln(os.Stderr, "run: go run ./cmd/vendorhash update")
|
||||
// Also emit machine-parseable lines so CI can pick them up.
|
||||
fmt.Printf("expected_sri=%s\n", hashes.Vendor.SRI)
|
||||
fmt.Printf("actual_sri=%s\n", curSRI)
|
||||
|
||||
return errStale
|
||||
}
|
||||
|
||||
func cmdUpdate(ctx context.Context) error {
|
||||
fp, err := goModFingerprint()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sri, err := hashVendor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeHashes(FlakeHashes{
|
||||
Vendor: VendorBlock{
|
||||
GoModSum: fp,
|
||||
SRI: sri,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// goModFingerprint returns a content fingerprint of go.mod and go.sum
|
||||
// that changes whenever either file changes. The byte layout matches
|
||||
// upstream tailscale's tool/updateflakes.
|
||||
func goModFingerprint() (string, error) {
|
||||
h := sha256.New()
|
||||
|
||||
for _, f := range []string{goModFile, goSumFile} {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Fprintf(h, "%s %d\n", f, len(b))
|
||||
h.Write(b)
|
||||
}
|
||||
|
||||
return "sha256-" + base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// hashVendor runs `go mod vendor` into a temporary directory and
|
||||
// returns the Nix SRI hash of the resulting tree.
|
||||
func hashVendor(ctx context.Context) (string, error) {
|
||||
out, err := os.MkdirTemp("", "nar-vendor-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// `go mod vendor -o` requires the destination to not already exist.
|
||||
err = os.Remove(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer os.RemoveAll(out)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "go", "mod", "vendor", "-o", out)
|
||||
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("go mod vendor: %w", err)
|
||||
}
|
||||
|
||||
return nardump.SRI(os.DirFS(out))
|
||||
}
|
||||
|
||||
func loadHashes() (FlakeHashes, error) {
|
||||
var h FlakeHashes
|
||||
|
||||
b, err := os.ReadFile(hashesFile)
|
||||
if err != nil {
|
||||
return h, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &h)
|
||||
if err != nil {
|
||||
return h, fmt.Errorf("%s: %w", hashesFile, err)
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func writeHashes(h FlakeHashes) error {
|
||||
b, err := json.MarshalIndent(h, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b = append(b, '\n')
|
||||
|
||||
// flakehashes.json is committed source read by Nix during evaluation;
|
||||
// world-readable matches every other tracked file in the repo.
|
||||
return os.WriteFile(hashesFile, b, 0o644) //nolint:gosec
|
||||
}
|
||||
14
flake.nix
14
flake.nix
@@ -27,7 +27,7 @@
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${prev.stdenv.hostPlatform.system};
|
||||
buildGo = pkgs.buildGo126Module;
|
||||
vendorHash = "sha256-Jquzx8xIkV28S8DnZVH8apQ4q+Q92e5yVX13fxodw8Y=";
|
||||
vendorHash = (builtins.fromJSON (builtins.readFile ./flakehashes.json)).vendor.sri;
|
||||
in
|
||||
{
|
||||
headscale = buildGo {
|
||||
@@ -38,8 +38,8 @@
|
||||
# Only run unit tests when testing a build
|
||||
checkFlags = [ "-short" ];
|
||||
|
||||
# When updating go.mod or go.sum, a new sha will need to be calculated,
|
||||
# update this if you have a mismatch after doing a change to those files.
|
||||
# vendorHash is read from flakehashes.json; refresh via:
|
||||
# go run ./cmd/vendorhash update
|
||||
inherit vendorHash;
|
||||
|
||||
subPackages = [ "cmd/headscale" ];
|
||||
@@ -223,13 +223,7 @@
|
||||
"nix-vendor-sri"
|
||||
''
|
||||
set -eu
|
||||
|
||||
OUT=$(mktemp -d -t nar-hash-XXXXXX)
|
||||
rm -rf "$OUT"
|
||||
|
||||
go mod vendor -o "$OUT"
|
||||
go run tailscale.com/cmd/nardump --sri "$OUT"
|
||||
rm -rf "$OUT"
|
||||
exec go run ./cmd/vendorhash update "$@"
|
||||
'')
|
||||
|
||||
(pkgs.writeShellScriptBin
|
||||
|
||||
6
flakehashes.json
Normal file
6
flakehashes.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"vendor": {
|
||||
"goModSum": "sha256-IE0n9cSqO4XNX4RN+CGBk9VC46iACiZKDFf/215iivk=",
|
||||
"sri": "sha256-ijEIP9NSomhlWOgsVN7tPvSuvkTiLtnvXvhZmatIDLM="
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user