diff --git a/core/cli/run.go b/core/cli/run.go index 4608d0858..491c429d4 100644 --- a/core/cli/run.go +++ b/core/cli/run.go @@ -35,7 +35,8 @@ import ( type RunCMD struct { AddonHostOps string `name:"addon-host-ops" env:"LOCALAI_ADDON_HOST_OPS" default:"fine-tuning=finetune.,quantization=quantize." help:"Add-ons that may call host operations, as a comma list of addon-id=operation-prefix. An add-on that is not listed gets no host operation channel. Empty gives none" group:"addons"` AddonsEnabled bool `name:"addons-enabled" env:"LOCALAI_ADDONS_ENABLED" default:"true" help:"Run the add-on owner, which installs and supervises add-ons. Turn it off with --addons-enabled=false" group:"addons"` - AddonsOwnershipMode string `name:"addons-ownership-mode" env:"LOCALAI_ADDONS_OWNERSHIP_MODE" default:"named-user" help:"Who may use add-ons: named-user (each account has its own access; needs authentication) or local-owner (every caller shares one owner)" group:"addons"` + AddonsOwnershipMode string `name:"addons-ownership-mode" env:"LOCALAI_ADDONS_OWNERSHIP_MODE" help:"Who may use add-ons: named-user (each account has its own access; needs authentication) or local-owner (every caller shares one owner). Default: local-owner when authentication is off, named-user when it is on" group:"addons"` + AddonsOfficialLibrary bool `name:"addons-official-library" env:"LOCALAI_ADDONS_OFFICIAL_LIBRARY" default:"true" help:"Register the official add-on library on the first start, when the add-on owner has no sources.json yet. A library the admin removes later stays removed. Turn it off with --addons-official-library=false" group:"addons"` Pprof bool `name:"pprof" env:"LOCALAI_PPROF" default:"false" help:"Enable the private loopback profiling listener" group:"diagnostics"` PprofAddress string `name:"pprof-address" env:"LOCALAI_PPROF_ADDRESS" default:"127.0.0.1:6060" help:"Numeric loopback address and port for profiling" group:"diagnostics"` PprofMutexProfileFraction int `name:"pprof-mutex-profile-fraction" env:"LOCALAI_PPROF_MUTEX_PROFILE_FRACTION" default:"0" help:"Mutex sampling fraction (0 disables sampling); requires pprof" group:"diagnostics"` @@ -662,6 +663,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) { return err } if addonCfg.Enabled { + addonCfg.OfficialLibrary = r.AddonsOfficialLibrary opts = append(opts, config.WithAddons(addonCfg)) } if authEnabled { diff --git a/core/cli/run_addons.go b/core/cli/run_addons.go index 8c76f070b..260123946 100644 --- a/core/cli/run_addons.go +++ b/core/cli/run_addons.go @@ -19,14 +19,28 @@ const localAddonOwnerID = "local" // the run flags. Only the on/off switch and the ownership mode are flags; the // rest follows from the node being its own owner. // -// The default mode, named-user, needs the account database. Without -// authentication there is no such database, so the owner stays off with a -// warning instead of stopping the server: local-owner mode is never inferred -// and an operator must select it. +// An empty mode takes the documented default: local-owner when authentication +// is off, named-user when it is on. An explicit mode always wins. Named-user +// needs the account database, so an explicit named-user without +// authentication leaves the owner off with a warning instead of stopping the +// server. func addonConfigFor(enabled bool, mode, dataPath string, authEnabled bool) (config.AddonConfig, error) { if !enabled { return config.AddonConfig{}, nil } + if mode == "" { + // The documented default for an install that did not choose a mode. + // With authentication off there are no accounts, so the one shared + // local owner is what makes add-ons usable; with authentication on, + // every account keeps its own access. This is the one place that + // resolves the default. It is an explicit, documented default for + // auth-off installs, not an inference from the request or the network, + // and a flag or environment value always wins over it. + mode = auth.AddonModeNamedUser + if !authEnabled { + mode = auth.AddonModeLocalOwner + } + } switch mode { case auth.AddonModeNamedUser, auth.AddonModeLocalOwner: default: diff --git a/core/cli/run_addons_test.go b/core/cli/run_addons_test.go index f5c49bf38..d5bf3551b 100644 --- a/core/cli/run_addons_test.go +++ b/core/cli/run_addons_test.go @@ -32,7 +32,7 @@ var _ = Describe("Add-on owner flags", Serial, func() { return &command.Run, err } BeforeEach(func() { - for _, key := range []string{"LOCALAI_ADDONS_ENABLED", "LOCALAI_ADDONS_OWNERSHIP_MODE"} { + for _, key := range []string{"LOCALAI_ADDONS_ENABLED", "LOCALAI_ADDONS_OWNERSHIP_MODE", "LOCALAI_ADDONS_OFFICIAL_LIBRARY"} { value, present := os.LookupEnv(key) DeferCleanup(func() { if present { @@ -45,11 +45,12 @@ var _ = Describe("Add-on owner flags", Serial, func() { } }) - It("turns the owner on by default, in named-user mode", func() { + It("turns the owner on by default, in named-user mode when authentication is on", func() { r, err := parse() Expect(err).NotTo(HaveOccurred()) Expect(r.AddonsEnabled).To(BeTrue()) - Expect(r.AddonsOwnershipMode).To(Equal("named-user")) + Expect(r.AddonsOwnershipMode).To(BeEmpty()) + Expect(r.AddonsOfficialLibrary).To(BeTrue()) dir := GinkgoT().TempDir() c, err := addonConfigFor(r.AddonsEnabled, r.AddonsOwnershipMode, dir, true) Expect(err).NotTo(HaveOccurred()) @@ -65,6 +66,32 @@ var _ = Describe("Add-on owner flags", Serial, func() { Expect(c.Transport.OwnerAddress).To(Equal(c.OwnerAddress)) }) + It("defaults to local-owner when authentication is off", func() { + r, err := parse() + Expect(err).NotTo(HaveOccurred()) + c, err := addonConfigFor(r.AddonsEnabled, r.AddonsOwnershipMode, GinkgoT().TempDir(), false) + Expect(err).NotTo(HaveOccurred()) + Expect(c.Enabled).To(BeTrue()) + Expect(c.OwnershipMode).To(Equal("local-owner")) + }) + + It("lets an explicit named-user win over the auth-off default", func() { + r, err := parse("--addons-ownership-mode=named-user") + Expect(err).NotTo(HaveOccurred()) + c, err := addonConfigFor(r.AddonsEnabled, r.AddonsOwnershipMode, GinkgoT().TempDir(), false) + Expect(err).NotTo(HaveOccurred()) + Expect(c).To(Equal(config.AddonConfig{})) + }) + + It("reads the ownership mode and the official library from the environment", func() { + Expect(os.Setenv("LOCALAI_ADDONS_OWNERSHIP_MODE", "named-user")).To(Succeed()) + Expect(os.Setenv("LOCALAI_ADDONS_OFFICIAL_LIBRARY", "false")).To(Succeed()) + r, err := parse() + Expect(err).NotTo(HaveOccurred()) + Expect(r.AddonsOwnershipMode).To(Equal("named-user")) + Expect(r.AddonsOfficialLibrary).To(BeFalse()) + }) + It("turns the owner off completely with --addons-enabled=false", func() { r, err := parse("--addons-enabled=false") Expect(err).NotTo(HaveOccurred()) diff --git a/core/config/application_config.go b/core/config/application_config.go index e977c0bbd..acadb3b96 100644 --- a/core/config/application_config.go +++ b/core/config/application_config.go @@ -26,12 +26,18 @@ type AddonConfig struct { InstanceID, OwnerID, OwnerAddress, StorageID, Root string SharedStorage bool // OwnershipMode is "named-user" (the default when empty) or "local-owner". - // Local-owner mode is explicit opt-in and never inferred: every caller that - // reaches the addon routes shares one authority and sharing is disabled. + // Local-owner mode is never inferred from a request. The CLI sets it as the + // documented default when authentication is off and the operator chose no + // mode. Every caller that reaches the addon routes shares one authority and + // sharing is disabled. // Every frontend of a deployment must use the same value. OwnershipMode string - Host contract.HostCompatibility - AdminCheck func(context.Context, contract.Actor) error `json:"-" yaml:"-"` + // OfficialLibrary registers the official add-on library on the first start, + // when the owner has no sources.json yet. A sources.json that exists is + // never changed by it, so a library the admin removed stays removed. + OfficialLibrary bool + Host contract.HostCompatibility + AdminCheck func(context.Context, contract.Actor) error `json:"-" yaml:"-"` // BackupKeep is how many backups of one add-on the owner keeps; zero is // three. BackupMaxAge removes older ones; zero keeps them by count only. // The newest verified backup and the recovery copy of an upgrade are diff --git a/core/http/endpoints/addons/gateway.go b/core/http/endpoints/addons/gateway.go index 1c8ec8ee5..4f1b94343 100644 --- a/core/http/endpoints/addons/gateway.go +++ b/core/http/endpoints/addons/gateway.go @@ -60,7 +60,8 @@ type Option func(*settings) type settings struct{ mode string } // WithOwnership selects the ownership mode the deployment configured. The -// default is named-user; local-owner is never inferred from a request. +// default is named-user; local-owner is never inferred from a request. The +// CLI selects it as the documented default of an install with authentication off. func WithOwnership(mode string) Option { return func(s *settings) { s.mode = mode } }