Files
LocalAI/pkg/addons/packaging/layout.go
T
Ettore Di Giacinto 1604a67c1f feat(addons): add the add-on runtime, navigation and first-party add-ons
Add the add-on runtime to LocalAI: manifests, the host operation broker,
admin gating by account role, the add-on UI bundle builder and the SDK
for add-on pages. Move the navigation to Chat, Models, Add-ons and
Settings, with pinned add-ons in the sidebar and an Add-ons drawer.

Ship the first-party add-ons (voice, face, fine-tuning, quantization) as
packages for linux/amd64, linux/arm64 and darwin/arm64. Intel macOS
(darwin/amd64) is enabled in the platform files.

Squashed from the review branch. The tree is unchanged; the pre-squash
history is kept on a local backup branch.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-10-20 20:54:43 +00:00

187 lines
5.8 KiB
Go

// SPDX-License-Identifier: MIT
package packaging
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
// Roles of the layers of a platform image, in order. The ui layer exists only
// when the manifest declares a UI.
const (
roleExecutable = "executable"
roleUI = "ui"
roleManifest = "manifest"
)
// pkgError carries a stable code so a test (and a reader of CI logs) can tell
// which package rule failed.
type pkgError struct{ code, msg string }
func (e *pkgError) Error() string { return e.code + ": " + e.msg }
func fail(code, format string, a ...any) error {
return &pkgError{code: code, msg: fmt.Sprintf(format, a...)}
}
// Receipt is the digest receipt written next to the layout.
type Receipt struct {
Schema int `json:"schema"`
Addon string `json:"addon"`
Version string `json:"version"`
Platform string `json:"platform"`
Revision string `json:"revision,omitempty"`
Publishable bool `json:"publishable"`
ManifestDigest string `json:"manifestDigest"`
ConfigDigest string `json:"configDigest"`
Layers []ReceiptLayer `json:"layers"`
}
type ReceiptLayer struct {
Role string `json:"role"`
Digest string `json:"digest"`
Size int64 `json:"size"`
MediaType string `json:"mediaType"`
}
const receiptFile = "receipt.json"
// leg is one platform image read from a layout, with every blob re-hashed.
type leg struct {
dir string
descriptor v1.Descriptor
manifest v1.Manifest
rawMan []byte
config v1.ConfigFile
}
func strictJSON(b []byte, v any) error {
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
if err := d.Decode(v); err != nil {
return err
}
if d.More() {
return errors.New("trailing data")
}
return nil
}
// readBlob returns the bytes of a blob after checking digest and size.
func readBlob(dir string, d v1.Hash, size int64) ([]byte, error) {
if d.Algorithm != "sha256" || len(d.Hex) != 64 {
return nil, fail("E_DIGEST", "unsupported digest %q", d.String())
}
b, err := os.ReadFile(filepath.Join(dir, "blobs", d.Algorithm, d.Hex)) // #nosec G304 -- the packaging tool reads paths the operator passes on its command line
if err != nil {
return nil, fail("E_DIGEST", "blob %s is missing", d)
}
sum := sha256.Sum256(b)
if hex.EncodeToString(sum[:]) != d.Hex {
return nil, fail("E_DIGEST", "blob %s does not hash to its digest", d)
}
if size >= 0 && int64(len(b)) != size {
return nil, fail("E_DIGEST", "blob %s has size %d, descriptor says %d", d, len(b), size)
}
return b, nil
}
func readRootIndex(dir string) (*v1.IndexManifest, error) {
marker, err := os.ReadFile(filepath.Join(dir, "oci-layout")) // #nosec G304 -- the packaging tool reads paths the operator passes on its command line
if err != nil {
return nil, fail("E_LAYOUT", "oci-layout file is missing")
}
var m struct {
Version string `json:"imageLayoutVersion"`
}
if err := strictJSON(marker, &m); err != nil || m.Version != "1.0.0" {
return nil, fail("E_LAYOUT", "unsupported oci-layout marker")
}
b, err := os.ReadFile(filepath.Join(dir, "index.json")) // #nosec G304 -- the packaging tool reads paths the operator passes on its command line
if err != nil {
return nil, fail("E_LAYOUT", "index.json is missing")
}
var idx v1.IndexManifest
if err := strictJSON(b, &idx); err != nil {
return nil, fail("E_LAYOUT", "index.json: %v", err)
}
if idx.SchemaVersion != 2 {
return nil, fail("E_LAYOUT", "index.json schemaVersion %d", idx.SchemaVersion)
}
return &idx, nil
}
// loadManifestBlob reads one image manifest and its config by digest.
func loadLegByDescriptor(dir string, d v1.Descriptor) (*leg, error) {
if d.MediaType != types.OCIManifestSchema1 {
return nil, fail("E_LAYERS", "manifest media type %q is not an OCI image manifest", d.MediaType)
}
raw, err := readBlob(dir, d.Digest, d.Size)
if err != nil {
return nil, err
}
l := &leg{dir: dir, descriptor: d, rawMan: raw}
if err := strictJSON(raw, &l.manifest); err != nil {
return nil, fail("E_LAYERS", "image manifest: %v", err)
}
if l.manifest.MediaType != types.OCIManifestSchema1 {
return nil, fail("E_LAYERS", "image manifest mediaType %q", l.manifest.MediaType)
}
cfg, err := readBlob(dir, l.manifest.Config.Digest, l.manifest.Config.Size)
if err != nil {
return nil, err
}
if l.manifest.Config.MediaType != types.OCIConfigJSON {
return nil, fail("E_LAYERS", "config media type %q", l.manifest.Config.MediaType)
}
if err := json.Unmarshal(cfg, &l.config); err != nil {
return nil, fail("E_LAYERS", "image config: %v", err)
}
return l, nil
}
func loadLeg(dir string) (*leg, *Receipt, error) {
idx, err := readRootIndex(dir)
if err != nil {
return nil, nil, err
}
if len(idx.Manifests) != 1 {
return nil, nil, fail("E_LAYOUT", "a platform layout holds exactly one image, found %d", len(idx.Manifests))
}
l, err := loadLegByDescriptor(dir, idx.Manifests[0])
if err != nil {
return nil, nil, err
}
b, err := os.ReadFile(filepath.Join(dir, receiptFile)) // #nosec G304 -- the packaging tool reads paths the operator passes on its command line
if err != nil {
return nil, nil, fail("E_DIGEST", "digest receipt is missing")
}
var r Receipt
if err := strictJSON(b, &r); err != nil {
return nil, nil, fail("E_DIGEST", "digest receipt: %v", err)
}
return l, &r, nil
}
// copyBytes is io.Copy into a buffer; kept tiny so call sites read clearly.
func readAll(r io.Reader, limit int64) ([]byte, error) {
b, err := io.ReadAll(io.LimitReader(r, limit+1))
if err != nil {
return nil, err
}
if int64(len(b)) > limit {
return nil, errors.New("too large")
}
return b, nil
}