Migration files restructured with shared helpers; verified byte-identical SQLite schema vs previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
158 lines
4.6 KiB
Go
158 lines
4.6 KiB
Go
package vmsup
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/v10networks/nmon/internal/config"
|
|
)
|
|
|
|
// Environment overrides for development and tests built without vmembed.
|
|
const (
|
|
envBinary = "NMON_VM_BINARY"
|
|
envUnverified = "NMON_VM_BINARY_UNVERIFIED" // "1": skip the pin check (tests only)
|
|
)
|
|
|
|
// maxBinarySize bounds decompression so a corrupt .gz cannot fill the disk.
|
|
const maxBinarySize = 512 << 20
|
|
|
|
var (
|
|
// ErrChecksum means the store binary does not match its pinned SHA-256.
|
|
// nmon refuses to start in that case.
|
|
ErrChecksum = errors.New("vmsup: store binary failed its checksum")
|
|
// ErrNoBinary means a store must be started but this build has none.
|
|
ErrNoBinary = errors.New("vmsup: no embedded store binary in this build; set --vm-hot-url and --vm-long-url")
|
|
)
|
|
|
|
// binarySpec says which file to run and which hash it must have ("" = unverified).
|
|
type binarySpec struct {
|
|
path string
|
|
want string
|
|
}
|
|
|
|
// verify hashes the file at s.path against s.want. It runs before every start.
|
|
func (s binarySpec) verify() error {
|
|
if s.want == "" {
|
|
return nil
|
|
}
|
|
got, err := fileSHA256(s.path)
|
|
if err != nil {
|
|
return fmt.Errorf("vmsup: hash %s: %w", s.path, err)
|
|
}
|
|
if got != s.want {
|
|
return fmt.Errorf("%w: %s has sha256 %s, want %s", ErrChecksum, s.path, got, s.want)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// prepareBinary decides which binary to run, extracting the embedded one into
|
|
// <data-dir>/bin when needed, and verifies it.
|
|
func prepareBinary(dataDir string) (binarySpec, error) {
|
|
if p := os.Getenv(envBinary); p != "" {
|
|
return prepareSuppliedBinary(p)
|
|
}
|
|
if len(embeddedGz) == 0 {
|
|
return binarySpec{}, ErrNoBinary
|
|
}
|
|
dst := filepath.Join(config.BinDir(dataDir), "victoria-metrics-prod")
|
|
spec := binarySpec{path: dst, want: embeddedBinSHA256}
|
|
if err := extract(embeddedGz, spec); err != nil {
|
|
return spec, err
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
// prepareSuppliedBinary verifies a binary named by NMON_VM_BINARY against the
|
|
// pinned hash, unless the test-only override disables the check.
|
|
func prepareSuppliedBinary(path string) (binarySpec, error) {
|
|
spec := binarySpec{path: path}
|
|
if os.Getenv(envUnverified) != "1" {
|
|
spec.want = binPin(runtime.GOARCH)
|
|
if runtime.GOOS != "linux" || spec.want == "" {
|
|
return spec, fmt.Errorf("%w: no pinned hash for %s/%s; set %s=1 for tests",
|
|
ErrChecksum, runtime.GOOS, runtime.GOARCH, envUnverified)
|
|
}
|
|
}
|
|
if err := spec.verify(); err != nil {
|
|
return spec, err
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
// extract writes the gunzipped data to spec.path (mode 0755) unless a file with
|
|
// the right hash is already there. It writes a temp file in the same directory,
|
|
// verifies it, and renames it into place, so a bad or partial file is never
|
|
// left at the final path.
|
|
func extract(gz []byte, spec binarySpec) error {
|
|
if _, err := os.Stat(spec.path); err == nil {
|
|
if spec.verify() == nil {
|
|
return nil
|
|
}
|
|
}
|
|
dir := filepath.Dir(spec.path)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("vmsup: create %s: %w", dir, err)
|
|
}
|
|
tmp, err := os.CreateTemp(dir, ".victoria-metrics-prod-*")
|
|
if err != nil {
|
|
return fmt.Errorf("vmsup: create temp binary: %w", err)
|
|
}
|
|
tmpName := tmp.Name()
|
|
defer func() { _ = os.Remove(tmpName) }() // no-op after a successful rename
|
|
|
|
zr, err := gzip.NewReader(bytes.NewReader(gz))
|
|
if err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("%w: embedded archive: %w", ErrChecksum, err)
|
|
}
|
|
h := sha256.New()
|
|
n, err := io.Copy(io.MultiWriter(tmp, h), io.LimitReader(zr, maxBinarySize+1))
|
|
if err == nil && n > maxBinarySize {
|
|
err = errors.New("embedded binary is implausibly large")
|
|
}
|
|
if err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("%w: unpack embedded binary: %w", ErrChecksum, err)
|
|
}
|
|
if got := hex.EncodeToString(h.Sum(nil)); got != spec.want {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("%w: embedded binary has sha256 %s, want %s", ErrChecksum, got, spec.want)
|
|
}
|
|
if err := tmp.Chmod(0o755); err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("vmsup: chmod binary: %w", err)
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("vmsup: sync binary: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("vmsup: write binary: %w", err)
|
|
}
|
|
if err := os.Rename(tmpName, spec.path); err != nil {
|
|
return fmt.Errorf("vmsup: install binary: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fileSHA256(path string) (string, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil)), nil
|
|
}
|