mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 17:18:49 -04:00
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>
187 lines
5.8 KiB
Go
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
|
|
}
|