Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <[email protected]>
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package bundle
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// ErrLimit reports that a transport, archive or extraction budget was exceeded.
|
|
var ErrLimit = errors.New("bundle: resource limit exceeded")
|
|
|
|
// Limits bounds both archive transport and decoded tar data, including metadata.
|
|
// Zero fields select defaults. These ceilings permit large (1M-prefix) configs;
|
|
// they are safety ceilings, not a promise of suitability for a small-RAM node.
|
|
type Limits struct {
|
|
MaxCompressedBytes int64
|
|
MaxExpandedBytes int64
|
|
MaxFileBytes int64
|
|
MaxManifestBytes int64
|
|
MaxEntries int
|
|
MaxPathBytes int
|
|
}
|
|
|
|
// DefaultLimits permits large production configs while bounding untrusted input.
|
|
func DefaultLimits() Limits {
|
|
return Limits{MaxCompressedBytes: 128 << 20, MaxExpandedBytes: 512 << 20, MaxFileBytes: 256 << 20, MaxManifestBytes: 2 << 20, MaxEntries: 4096, MaxPathBytes: 1024}
|
|
}
|
|
|
|
// Normalized fills zero fields with defaults and rejects invalid budgets.
|
|
func (l Limits) Normalized() (Limits, error) {
|
|
d := DefaultLimits()
|
|
values := []struct {
|
|
v *int64
|
|
fallback int64
|
|
}{{&l.MaxCompressedBytes, d.MaxCompressedBytes}, {&l.MaxExpandedBytes, d.MaxExpandedBytes}, {&l.MaxFileBytes, d.MaxFileBytes}, {&l.MaxManifestBytes, d.MaxManifestBytes}}
|
|
for _, x := range values {
|
|
if *x.v < 0 || *x.v == int64(^uint64(0)>>1) {
|
|
return l, fmt.Errorf("bundle: invalid byte limit")
|
|
}
|
|
if *x.v == 0 {
|
|
*x.v = x.fallback
|
|
}
|
|
}
|
|
if l.MaxEntries < 0 || l.MaxPathBytes < 0 {
|
|
return l, fmt.Errorf("bundle: invalid entry/path limit")
|
|
}
|
|
if l.MaxEntries == 0 {
|
|
l.MaxEntries = d.MaxEntries
|
|
}
|
|
if l.MaxPathBytes == 0 {
|
|
l.MaxPathBytes = d.MaxPathBytes
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
func validatePath(name string, limit int) error {
|
|
if name == "" || len(name) > limit || path.IsAbs(name) || path.Clean(name) != name || name == "." || strings.ContainsAny(name, "\\:\x00") {
|
|
return fmt.Errorf("bundle: unsafe path %q", name)
|
|
}
|
|
for _, part := range strings.Split(name, "/") {
|
|
if part == ".." || strings.TrimRight(part, " .") != part {
|
|
return fmt.Errorf("bundle: unsafe path %q", name)
|
|
}
|
|
base := strings.ToUpper(strings.SplitN(part, ".", 2)[0])
|
|
if base == "CON" || base == "PRN" || base == "AUX" || base == "NUL" || (len(base) == 4 && (strings.HasPrefix(base, "COM") || strings.HasPrefix(base, "LPT")) && base[3] >= '1' && base[3] <= '9') {
|
|
return fmt.Errorf("bundle: reserved path %q", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type limitedWriter struct {
|
|
w io.Writer
|
|
remaining int64
|
|
}
|
|
|
|
func (w *limitedWriter) Write(p []byte) (int, error) {
|
|
if int64(len(p)) > w.remaining {
|
|
return 0, ErrLimit
|
|
}
|
|
n, err := w.w.Write(p)
|
|
w.remaining -= int64(n)
|
|
return n, err
|
|
}
|
|
|
|
// ReadCompressed rejects oversized input while reading, including chunked HTTP
|
|
// bodies and local files whose size changes after stat.
|
|
func ReadCompressed(r io.Reader, limits Limits) ([]byte, error) {
|
|
l, err := limits.Normalized()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := io.ReadAll(io.LimitReader(r, l.MaxCompressedBytes+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(b)) > l.MaxCompressedBytes {
|
|
return nil, ErrLimit
|
|
}
|
|
return b, nil
|
|
}
|