feat: backend native executor implement

This commit is contained in:
2026-08-16 01:27:30 +08:00
parent fbce91d797
commit 390e0565d3
44 changed files with 6430 additions and 31 deletions
+483
View File
@@ -0,0 +1,483 @@
// Package updatepackage reads the exact repack ZIP format currently emitted by deploy.
package updatepackage
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"yms-daemon/internal/filestore"
)
const (
manifestName = "artifact-selection.json"
backendArtifactKind = "BACKEND"
nativeArtifactType = "native"
maximumManifestBytes = 1 << 20
)
// BackendNativePackage is an opened repack ZIP containing one exact native backend artifact.
type BackendNativePackage struct {
archive *zip.ReadCloser
artifactEntry *zip.File
PackagePath string
PackageSHA256 string
CustomerCode string
VersionID string
ArtifactID int64
FileName string
Identity filestore.Identity
}
// OpenBackendNative validates the ZIP structure and selects one explicitly declared native backend artifact.
func OpenBackendNative(packagePath string) (*BackendNativePackage, error) {
if err := validateAbsoluteRegularFile(packagePath, "update package"); err != nil {
return nil, err
}
packageDigest, err := hashFile(packagePath)
if err != nil {
return nil, err
}
archive, err := zip.OpenReader(packagePath)
if err != nil {
return nil, fmt.Errorf("open update package ZIP %s: %w", packagePath, err)
}
closeOnError := func(cause error) (*BackendNativePackage, error) {
return nil, errors.Join(cause, archive.Close())
}
entries, err := validateEntries(archive.File)
if err != nil {
return closeOnError(err)
}
manifestEntry, found := entries[manifestName]
if !found {
return closeOnError(errors.New("update package is missing artifact-selection.json"))
}
manifest, err := decodeManifest(manifestEntry)
if err != nil {
return closeOnError(err)
}
selected, err := selectNativeBackend(manifest.BackendArtifacts)
if err != nil {
return closeOnError(err)
}
if err := validateRootFileName(selected.FileName); err != nil {
return closeOnError(fmt.Errorf("invalid backendArtifacts.fileName: %w", err))
}
artifactEntry, found := entries[selected.FileName]
if !found {
return closeOnError(fmt.Errorf("native backend artifact declared by backendArtifacts.fileName is missing: %s", selected.FileName))
}
if artifactEntry.FileInfo().IsDir() {
return closeOnError(fmt.Errorf("native backend artifact is not a file: %s", selected.FileName))
}
identity := filestore.Identity{
Size: int64(artifactEntry.UncompressedSize64),
SHA256: selected.SHA256,
}
if err := identity.Validate(); err != nil {
return closeOnError(fmt.Errorf("invalid backendArtifacts.sha256: %w", err))
}
return &BackendNativePackage{
archive: archive,
artifactEntry: artifactEntry,
PackagePath: packagePath,
PackageSHA256: packageDigest,
CustomerCode: manifest.CustomerCode,
VersionID: manifest.VersionID,
ArtifactID: selected.ID,
FileName: selected.FileName,
Identity: identity,
}, nil
}
// Close releases the opened ZIP file.
func (p *BackendNativePackage) Close() error {
if p == nil || p.archive == nil {
return nil
}
archive := p.archive
p.archive = nil
p.artifactEntry = nil
return archive.Close()
}
// ExtractArtifact writes the selected JAR to destination and verifies size and SHA-256 before publishing it.
func (p *BackendNativePackage) ExtractArtifact(destination string) error {
if p == nil || p.archive == nil || p.artifactEntry == nil {
return errors.New("native backend update package is not open")
}
if !filepath.IsAbs(destination) || filepath.Clean(destination) != destination {
return errors.New("native backend extraction destination must be a clean absolute path")
}
parent := filepath.Dir(destination)
if err := os.MkdirAll(parent, 0o750); err != nil {
return fmt.Errorf("create native backend extraction directory: %w", err)
}
parentInfo, err := os.Lstat(parent)
if err != nil {
return fmt.Errorf("inspect native backend extraction directory: %w", err)
}
if !parentInfo.IsDir() || parentInfo.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("native backend extraction parent is not a direct directory: %s", parent)
}
source, err := p.artifactEntry.Open()
if err != nil {
return fmt.Errorf("open native backend artifact %s: %w", p.FileName, err)
}
temporary, err := os.CreateTemp(parent, ".backend-jar-*")
if err != nil {
_ = source.Close()
return fmt.Errorf("create native backend extraction file: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if err := temporary.Chmod(0o640); err != nil {
_ = source.Close()
_ = temporary.Close()
return fmt.Errorf("set native backend extraction permissions: %w", err)
}
digest := sha256.New()
written, copyErr := io.Copy(io.MultiWriter(temporary, digest), source)
closeSourceErr := source.Close()
if err := errors.Join(copyErr, closeSourceErr); err != nil {
_ = temporary.Close()
return fmt.Errorf("extract native backend artifact: %w", err)
}
if written != p.Identity.Size {
_ = temporary.Close()
return fmt.Errorf("native backend artifact size mismatch: got %d, want %d", written, p.Identity.Size)
}
actualDigest := hex.EncodeToString(digest.Sum(nil))
if !strings.EqualFold(actualDigest, p.Identity.SHA256) {
_ = temporary.Close()
return fmt.Errorf("native backend artifact SHA-256 mismatch: got %s, want %s", actualDigest, p.Identity.SHA256)
}
if err := temporary.Sync(); err != nil {
_ = temporary.Close()
return fmt.Errorf("flush native backend extraction file: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close native backend extraction file: %w", err)
}
if err := os.Rename(temporaryPath, destination); err != nil {
return fmt.Errorf("publish native backend extraction file: %w", err)
}
return syncDirectory(parent)
}
type artifactSelectionManifest struct {
CustomerCode string `json:"customerCode"`
CustomerDisplayName *string `json:"customerDisplayName"`
VersionID string `json:"versionId"`
Items []string `json:"items"`
BackendArtifacts []manifestArtifact `json:"backendArtifacts"`
FrontendArtifacts []manifestArtifact `json:"frontendArtifacts"`
NodeSSRArtifacts []manifestArtifact `json:"nodeSsrArtifacts"`
Remark *string `json:"remark"`
}
type manifestArtifact struct {
ID int64 `json:"id"`
VersionCode *string `json:"versionCode"`
ArtifactKind string `json:"artifactKind"`
Type string `json:"type"`
SelectedType string `json:"selectedType"`
Platform *string `json:"platform"`
FileName string `json:"fileName"`
FilePath *string `json:"filePath"`
SHA256 string `json:"sha256"`
ImageRef *string `json:"imageRef"`
}
func decodeManifest(entry *zip.File) (artifactSelectionManifest, error) {
if entry.FileInfo().IsDir() {
return artifactSelectionManifest{}, errors.New("artifact-selection.json is not a file")
}
if entry.UncompressedSize64 > maximumManifestBytes {
return artifactSelectionManifest{}, errors.New("artifact-selection.json exceeds size limit")
}
reader, err := entry.Open()
if err != nil {
return artifactSelectionManifest{}, fmt.Errorf("open artifact-selection.json: %w", err)
}
defer reader.Close()
document, err := io.ReadAll(io.LimitReader(reader, maximumManifestBytes+1))
if err != nil {
return artifactSelectionManifest{}, fmt.Errorf("read artifact-selection.json: %w", err)
}
if len(document) > maximumManifestBytes {
return artifactSelectionManifest{}, errors.New("artifact-selection.json exceeds size limit")
}
if err := validateExactManifestJSON(document); err != nil {
return artifactSelectionManifest{}, fmt.Errorf("validate artifact-selection.json keys: %w", err)
}
var manifest artifactSelectionManifest
decoder := json.NewDecoder(bytes.NewReader(document))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&manifest); err != nil {
return artifactSelectionManifest{}, fmt.Errorf("decode artifact-selection.json: %w", err)
}
if err := ensureJSONEnd(decoder); err != nil {
return artifactSelectionManifest{}, err
}
if strings.TrimSpace(manifest.CustomerCode) == "" || strings.TrimSpace(manifest.VersionID) == "" {
return artifactSelectionManifest{}, errors.New("artifact-selection.json customerCode and versionId are required")
}
return manifest, nil
}
var rootManifestKeys = map[string]struct{}{
"customerCode": {},
"customerDisplayName": {},
"versionId": {},
"items": {},
"backendArtifacts": {},
"frontendArtifacts": {},
"nodeSsrArtifacts": {},
"remark": {},
}
var artifactManifestKeys = map[string]struct{}{
"id": {},
"versionCode": {},
"artifactKind": {},
"type": {},
"selectedType": {},
"platform": {},
"fileName": {},
"filePath": {},
"sha256": {},
"imageRef": {},
}
func validateExactManifestJSON(document []byte) error {
decoder := json.NewDecoder(bytes.NewReader(document))
decoder.UseNumber()
if err := validateObject(decoder, "", rootManifestKeys, true); err != nil {
return err
}
return ensureJSONEnd(decoder)
}
func validateObject(decoder *json.Decoder, objectPath string, allowed map[string]struct{}, root bool) error {
token, err := decoder.Token()
if err != nil {
return err
}
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
return fmt.Errorf("%s must be a JSON object", displayJSONPath(objectPath))
}
seen := make(map[string]struct{}, len(allowed))
for decoder.More() {
keyToken, err := decoder.Token()
if err != nil {
return err
}
key, ok := keyToken.(string)
if !ok {
return fmt.Errorf("%s contains a non-string key", displayJSONPath(objectPath))
}
fieldPath := key
if objectPath != "" {
fieldPath = objectPath + "." + key
}
if _, duplicate := seen[key]; duplicate {
return fmt.Errorf("duplicate JSON field %s", fieldPath)
}
seen[key] = struct{}{}
if _, known := allowed[key]; !known {
return fmt.Errorf("unknown JSON field %s", fieldPath)
}
if root && (key == "backendArtifacts" || key == "frontendArtifacts" || key == "nodeSsrArtifacts") {
if err := validateArtifactArray(decoder, fieldPath); err != nil {
return err
}
continue
}
if err := consumeJSONValue(decoder, fieldPath); err != nil {
return err
}
}
_, err = decoder.Token()
return err
}
func validateArtifactArray(decoder *json.Decoder, arrayPath string) error {
token, err := decoder.Token()
if err != nil {
return err
}
if delimiter, ok := token.(json.Delim); !ok || delimiter != '[' {
return fmt.Errorf("%s must be a JSON array", arrayPath)
}
index := 0
for decoder.More() {
if err := validateObject(decoder, fmt.Sprintf("%s[%d]", arrayPath, index), artifactManifestKeys, false); err != nil {
return err
}
index++
}
_, err = decoder.Token()
return err
}
func consumeJSONValue(decoder *json.Decoder, valuePath string) error {
token, err := decoder.Token()
if err != nil {
return err
}
delimiter, composite := token.(json.Delim)
if !composite {
return nil
}
switch delimiter {
case '[':
for decoder.More() {
if err := consumeJSONValue(decoder, valuePath); err != nil {
return err
}
}
_, err = decoder.Token()
return err
case '{':
seen := make(map[string]struct{})
for decoder.More() {
keyToken, err := decoder.Token()
if err != nil {
return err
}
key := keyToken.(string)
if _, duplicate := seen[key]; duplicate {
return fmt.Errorf("duplicate JSON field %s.%s", valuePath, key)
}
seen[key] = struct{}{}
if err := consumeJSONValue(decoder, valuePath+"."+key); err != nil {
return err
}
}
_, err = decoder.Token()
return err
default:
return fmt.Errorf("unexpected JSON delimiter %q at %s", delimiter, valuePath)
}
}
func displayJSONPath(value string) string {
if value == "" {
return "artifact-selection.json"
}
return value
}
func ensureJSONEnd(decoder *json.Decoder) error {
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("artifact-selection.json contains multiple JSON values")
}
return fmt.Errorf("decode artifact-selection.json trailing content: %w", err)
}
return nil
}
func selectNativeBackend(artifacts []manifestArtifact) (manifestArtifact, error) {
var selected manifestArtifact
count := 0
for _, artifact := range artifacts {
if artifact.ArtifactKind == backendArtifactKind && artifact.Type == nativeArtifactType && artifact.SelectedType == nativeArtifactType {
selected = artifact
count++
}
}
if count != 1 {
return manifestArtifact{}, fmt.Errorf("backendArtifacts must contain exactly one BACKEND native selection, got %d", count)
}
if selected.ID <= 0 || selected.FileName == "" || selected.SHA256 == "" {
return manifestArtifact{}, errors.New("selected native backend artifact requires id, fileName and sha256")
}
return selected, nil
}
func validateEntries(files []*zip.File) (map[string]*zip.File, error) {
entries := make(map[string]*zip.File, len(files))
for _, file := range files {
name := file.Name
if name == "" || strings.Contains(name, "\\") || path.IsAbs(name) || path.Clean(name) != name || name == "." || strings.HasPrefix(name, "../") {
return nil, fmt.Errorf("update package contains unsafe ZIP entry: %q", name)
}
if file.Mode()&os.ModeSymlink != 0 {
return nil, fmt.Errorf("update package contains symbolic link entry: %s", name)
}
if _, duplicate := entries[name]; duplicate {
return nil, fmt.Errorf("update package contains duplicate ZIP entry: %s", name)
}
entries[name] = file
}
return entries, nil
}
func validateRootFileName(name string) error {
if name == "" || strings.ContainsAny(name, "/\\") || path.Clean(name) != name || name == "." || name == ".." {
return fmt.Errorf("expected one exact ZIP root file name, got %q", name)
}
return nil
}
func validateAbsoluteRegularFile(filePath string, description string) error {
if !filepath.IsAbs(filePath) || filepath.Clean(filePath) != filePath {
return fmt.Errorf("%s path must be a clean absolute path", description)
}
info, err := os.Lstat(filePath)
if err != nil {
return fmt.Errorf("inspect %s %s: %w", description, filePath, err)
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%s is not a direct regular file: %s", description, filePath)
}
return nil
}
func hashFile(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("open update package for SHA-256: %w", err)
}
digest := sha256.New()
_, copyErr := io.Copy(digest, file)
closeErr := file.Close()
if err := errors.Join(copyErr, closeErr); err != nil {
return "", fmt.Errorf("hash update package: %w", err)
}
return hex.EncodeToString(digest.Sum(nil)), nil
}
func syncDirectory(directory string) error {
file, err := os.Open(directory)
if err != nil {
return fmt.Errorf("open extraction directory for flush: %w", err)
}
syncErr := file.Sync()
closeErr := file.Close()
if err := errors.Join(syncErr, closeErr); err != nil {
return fmt.Errorf("flush extraction directory: %w", err)
}
return nil
}