fix: harden model catalog projections
This commit is contained in:
@@ -21,6 +21,8 @@ const (
|
||||
ComposeFile = "compose.models.yaml"
|
||||
)
|
||||
|
||||
var renameProjectionDirectory = os.Rename
|
||||
|
||||
type runtimeCatalog struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
DefaultSession string `json:"defaultSession"`
|
||||
@@ -87,10 +89,11 @@ func Render(installation config.Installation) (map[string][]byte, error) {
|
||||
if model.Session == nil {
|
||||
continue
|
||||
}
|
||||
enabled = append(enabled, model.ID)
|
||||
if model.SessionAdapter == nil || model.SessionAdapter.Mode != "openai_compatible" {
|
||||
enabled = append(enabled, model.ID)
|
||||
continue
|
||||
}
|
||||
enabled = append(enabled, model.Provider+"/"+model.UpstreamModel)
|
||||
provider := customProviders[model.Provider]
|
||||
provider.BaseURL = model.Endpoint.BaseURL
|
||||
provider.API = "openai-completions"
|
||||
@@ -137,52 +140,84 @@ func Render(installation config.Installation) (map[string][]byte, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Generate atomically replaces each generated adapter after the complete candidate has rendered.
|
||||
// Generate publishes all adapters as one directory generation. A failed replacement restores the
|
||||
// previous directory, so callers never observe a successfully returned mixed generation.
|
||||
func Generate(installation config.Installation) error {
|
||||
artifacts, err := Render(installation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths := sortedArtifactPaths(artifacts)
|
||||
for _, relative := range paths {
|
||||
destination := filepath.Join(installation.GeneratedDirectory(), filepath.FromSlash(relative))
|
||||
// The generated catalog contains references to secret environment variable names, never
|
||||
// secret values. Core runs as an unprivileged container user and must be able to traverse
|
||||
// the bind-mounted host directories and read the projections.
|
||||
target := installation.GeneratedDirectory()
|
||||
parent := filepath.Dir(target)
|
||||
if err := os.MkdirAll(parent, 0o755); err != nil {
|
||||
return fmt.Errorf("create model projection parent: %w", err)
|
||||
}
|
||||
candidate, err := os.MkdirTemp(parent, ".model-projections-candidate-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create model projection candidate: %w", err)
|
||||
}
|
||||
defer func() { _ = os.RemoveAll(candidate) }()
|
||||
if err := writeProjectionCandidate(candidate, artifacts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, statErr := os.Lstat(target)
|
||||
if os.IsNotExist(statErr) {
|
||||
if err := renameProjectionDirectory(candidate, target); err != nil {
|
||||
return fmt.Errorf("publish model projection generation: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if statErr != nil {
|
||||
return fmt.Errorf("inspect current model projection generation: %w", statErr)
|
||||
}
|
||||
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("current model projection path is not a regular directory")
|
||||
}
|
||||
previous, err := absentTemporaryPath(parent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reserve previous model projection generation: %w", err)
|
||||
}
|
||||
if err := renameProjectionDirectory(target, previous); err != nil {
|
||||
return fmt.Errorf("prepare model projection generation replacement: %w", err)
|
||||
}
|
||||
if err := renameProjectionDirectory(candidate, target); err != nil {
|
||||
if restoreErr := renameProjectionDirectory(previous, target); restoreErr != nil {
|
||||
return fmt.Errorf("publish model projection generation: %v; restore previous generation: %w", err, restoreErr)
|
||||
}
|
||||
return fmt.Errorf("publish model projection generation: %w", err)
|
||||
}
|
||||
_ = os.RemoveAll(previous)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeProjectionCandidate(directory string, artifacts map[string][]byte) error {
|
||||
if err := os.Chmod(directory, 0o755); err != nil {
|
||||
return fmt.Errorf("protect model projection candidate: %w", err)
|
||||
}
|
||||
for _, relative := range sortedArtifactPaths(artifacts) {
|
||||
destination := filepath.Join(directory, filepath.FromSlash(relative))
|
||||
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
|
||||
return fmt.Errorf("create model projection directory: %w", err)
|
||||
}
|
||||
if err := os.Chmod(filepath.Dir(destination), 0o755); err != nil {
|
||||
return fmt.Errorf("protect model projection directory: %w", err)
|
||||
}
|
||||
temporary, err := os.CreateTemp(filepath.Dir(destination), ".projection-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create model projection candidate: %w", err)
|
||||
}
|
||||
temporaryName := temporary.Name()
|
||||
published := false
|
||||
defer func() {
|
||||
if !published {
|
||||
_ = os.Remove(temporaryName)
|
||||
}
|
||||
}()
|
||||
if err := temporary.Chmod(0o644); err == nil {
|
||||
_, err = temporary.Write(artifacts[relative])
|
||||
}
|
||||
if closeErr := temporary.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
if err := os.WriteFile(destination, artifacts[relative], 0o644); err != nil {
|
||||
return fmt.Errorf("write model projection candidate: %w", err)
|
||||
}
|
||||
if err := os.Rename(temporaryName, destination); err != nil {
|
||||
return fmt.Errorf("publish model projection: %w", err)
|
||||
}
|
||||
published = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func absentTemporaryPath(parent string) (string, error) {
|
||||
path, err := os.MkdirTemp(parent, ".model-projections-previous-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Check returns relative artifact names whose current bytes differ from the catalog projection.
|
||||
func Check(installation config.Installation) ([]string, error) {
|
||||
artifacts, err := Render(installation)
|
||||
|
||||
Reference in New Issue
Block a user