187 lines
4.1 KiB
Go
187 lines
4.1 KiB
Go
package forge
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"forge-mesh/internal/api/types"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var targets = []struct {
|
|
OS string
|
|
Arch string
|
|
}{
|
|
{"linux", "amd64"},
|
|
{"linux", "arm64"},
|
|
}
|
|
|
|
// Pipeline builds and optionally signs agent binaries.
|
|
type Pipeline struct {
|
|
db *sql.DB
|
|
artifactsDir string
|
|
signingKey *KeyPair
|
|
agentMainPath string
|
|
version string
|
|
}
|
|
|
|
func NewPipeline(db *sql.DB, artifactsDir, signingKeyPath, agentMainPath, version string) (*Pipeline, error) {
|
|
key, err := LoadOrCreateKey(signingKeyPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Pipeline{
|
|
db: db,
|
|
artifactsDir: artifactsDir,
|
|
signingKey: key,
|
|
agentMainPath: agentMainPath,
|
|
version: version,
|
|
}, nil
|
|
}
|
|
|
|
// BuildAll cross-compiles agent for linux amd64/arm64, signs, and stores artifacts.
|
|
func (p *Pipeline) BuildAll(public bool) ([]types.Build, error) {
|
|
var builds []types.Build
|
|
for _, tgt := range targets {
|
|
b, err := p.buildOne(tgt.OS, tgt.Arch, public)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
builds = append(builds, *b)
|
|
}
|
|
return builds, nil
|
|
}
|
|
|
|
func (p *Pipeline) buildOne(osName, arch string, public bool) (*types.Build, error) {
|
|
outName := fmt.Sprintf("forge-mesh-agent-%s-%s", osName, arch)
|
|
outPath := filepath.Join(p.artifactsDir, outName)
|
|
|
|
cmd := exec.Command("go", "build", "-trimpath", "-ldflags=-s -w",
|
|
"-o", outPath,
|
|
p.agentMainPath,
|
|
)
|
|
cmd.Env = append(os.Environ(),
|
|
"GOOS="+osName,
|
|
"GOARCH="+arch,
|
|
"CGO_ENABLED=0",
|
|
)
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("build %s/%s: %w\n%s", osName, arch, err, out)
|
|
}
|
|
|
|
data, err := os.ReadFile(outPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sum := sha256.Sum256(data)
|
|
checksum := hex.EncodeToString(sum[:])
|
|
sig := p.signingKey.Sign(data)
|
|
|
|
build := types.Build{
|
|
ID: uuid.NewString(),
|
|
OS: osName,
|
|
Arch: arch,
|
|
Version: p.version,
|
|
Checksum: checksum,
|
|
Signature: sig,
|
|
Public: public,
|
|
Path: outPath,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
if err := p.saveBuild(&build); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &build, nil
|
|
}
|
|
|
|
func (p *Pipeline) saveBuild(b *types.Build) error {
|
|
pub := 0
|
|
if b.Public {
|
|
pub = 1
|
|
}
|
|
_, err := p.db.Exec(`
|
|
INSERT INTO builds (id, os, arch, version, checksum, signature, public, path, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, b.ID, b.OS, b.Arch, b.Version, b.Checksum, b.Signature, pub, b.Path, b.CreatedAt.Format(time.RFC3339))
|
|
return err
|
|
}
|
|
|
|
// LatestPublic returns the newest public build for os/arch.
|
|
func LatestPublic(db *sql.DB, osName, arch string) (*types.Build, error) {
|
|
row := db.QueryRow(`
|
|
SELECT id, os, arch, version, checksum, signature, public, path, created_at
|
|
FROM builds
|
|
WHERE public = 1 AND os = ? AND arch = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
`, osName, arch)
|
|
|
|
var b types.Build
|
|
var pub int
|
|
var path sql.NullString
|
|
var createdAt string
|
|
|
|
err := row.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &b.Signature, &pub, &path, &createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.Public = pub == 1
|
|
if path.Valid {
|
|
b.Path = path.String
|
|
}
|
|
b.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
|
return &b, nil
|
|
}
|
|
|
|
// GetBuild loads a build by ID.
|
|
func GetBuild(db *sql.DB, id string) (*types.Build, error) {
|
|
row := db.QueryRow(`
|
|
SELECT id, os, arch, version, checksum, signature, public, path, created_at
|
|
FROM builds WHERE id = ?
|
|
`, id)
|
|
|
|
var b types.Build
|
|
var pub int
|
|
var path sql.NullString
|
|
var createdAt string
|
|
|
|
err := row.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &b.Signature, &pub, &path, &createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.Public = pub == 1
|
|
if path.Valid {
|
|
b.Path = path.String
|
|
}
|
|
b.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
|
return &b, nil
|
|
}
|
|
|
|
// PublicKey returns the pipeline signing public key hex.
|
|
func (p *Pipeline) PublicKey() string {
|
|
return p.signingKey.PublicKeyHex()
|
|
}
|
|
|
|
// CopyArtifact streams a build artifact to w.
|
|
func CopyArtifact(path string, w io.Writer) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(w, f)
|
|
return err
|
|
}
|