feat(cfg): snapshots.limit 0 now disables snapshots instead of clamping to 1
- SnapshotLimit() returns 0 when the configured limit is 0 (or negative), meaning users cannot create new snapshots; existing snapshots are kept - SnapshotCreate refuses with 'snapshots are disabled' before any Incus call - Update config comment/registry description and tests for the new semantics
This commit is contained in:
1 parent
bd5acddd15
commit
a883e272c9
5 files changed
+58
-12
No files matched your search
@@ -199,7 +199,9 @@ type SnapshotsCfg struct {
|
||||
// Limit caps how many snapshots a user may keep per container. The default
|
||||
// of 1 gives a light safety net; a snapshot is disk-only (no memory) and
|
||||
// lives under the container in Incus, so it is deleted together with the
|
||||
// container on reinstall/delete. 0 or negative is treated as 1.
|
||||
// container on reinstall/delete. 0 disables snapshots: users cannot create
|
||||
// new ones, but existing snapshots are left untouched. Negative is treated
|
||||
// as 0 (the config validator rejects negatives anyway).
|
||||
Limit int `yaml:"limit"`
|
||||
}
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ var Fields = []Field{
|
||||
return nil
|
||||
}},
|
||||
{"snapshots.limit", KindOperator, ApplyRestart,
|
||||
"max snapshots a user may keep per container (0 = default 1)",
|
||||
"max snapshots a user may keep per container (0 = disable snapshots)",
|
||||
"1",
|
||||
getStr(func(c *Config) string { return strconv.Itoa(c.Snapshots.Limit) }),
|
||||
func(c *Config, v string) error {
|
||||
|
||||
+12
-5
@@ -875,10 +875,13 @@ var validSnapName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$`)
|
||||
// API. Exported for tests and the panel.
|
||||
func ValidSnapName(v string) bool { return validSnapName.MatchString(v) }
|
||||
|
||||
// SnapshotLimit returns the configured per-container snapshot cap.
|
||||
// SnapshotLimit returns the configured per-container snapshot cap. 0 (or a
|
||||
// negative value, which the config validator forbids anyway) means snapshots
|
||||
// are disabled: users cannot create new ones, but existing snapshots are left
|
||||
// untouched. Any positive value is the hard cap.
|
||||
func (m *Manager) SnapshotLimit() int {
|
||||
if m.cfg.Snapshots.Limit < 1 {
|
||||
return 1
|
||||
if m.cfg.Snapshots.Limit < 0 {
|
||||
return 0
|
||||
}
|
||||
return m.cfg.Snapshots.Limit
|
||||
}
|
||||
@@ -903,12 +906,16 @@ func (m *Manager) SnapshotCreate(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
limit := m.SnapshotLimit()
|
||||
if limit == 0 {
|
||||
return errors.New("snapshots are disabled")
|
||||
}
|
||||
snaps, err := m.lx.SnapshotList(u.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(snaps) >= m.SnapshotLimit() {
|
||||
return fmt.Errorf("snapshot limit reached (%d)", m.SnapshotLimit())
|
||||
if len(snaps) >= limit {
|
||||
return fmt.Errorf("snapshot limit reached (%d)", limit)
|
||||
}
|
||||
return m.lx.SnapshotCreate(u.Name, snapName())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package mgr
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"vpsmgr/internal/cfg"
|
||||
"vpsmgr/internal/db"
|
||||
)
|
||||
|
||||
func TestValidSnapName(t *testing.T) {
|
||||
@@ -45,3 +49,25 @@ func TestSnapNameFormat(t *testing.T) {
|
||||
t.Error("snapName() returned the same value twice; concurrent creates would collide")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotCreateDisabled verifies that a snapshot cap of 0 refuses creation
|
||||
// outright (before any Incus call), while leaving existing snapshots untouched.
|
||||
func TestSnapshotCreateDisabled(t *testing.T) {
|
||||
c := cfg.Default()
|
||||
d, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer d.Close()
|
||||
if _, err := d.CreateUser("alice", "x", "10.115.0.2", 1, 30001, 10000, 1, 1024, 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := New(c, d)
|
||||
// A dead socket means any real Incus call would fail; with the cap disabled
|
||||
// we must refuse before reaching Incus at all.
|
||||
c.Incus.Socket = "/nonexistent/vpsmgr-test.sock"
|
||||
c.Snapshots.Limit = 0
|
||||
if err := m.SnapshotCreate("alice"); err == nil || !strings.Contains(err.Error(), "disabled") {
|
||||
t.Fatalf("SnapshotCreate with limit 0 = %v, want 'snapshots are disabled'", err)
|
||||
}
|
||||
}
|
||||
@@ -920,16 +920,27 @@ func TestSnapshotRoutesRegistered(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotLimitClamped checks the config-driven cap is clamped to >= 1.
|
||||
func TestSnapshotLimitClamped(t *testing.T) {
|
||||
// TestSnapshotLimitSemantics checks the config-driven snapshot cap: default is
|
||||
// 1, a positive value is the cap, and 0 (or negative) disables snapshots.
|
||||
func TestSnapshotLimitSemantics(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
if got := srv.mgr.SnapshotLimit(); got != 1 {
|
||||
t.Fatalf("SnapshotLimit() = %d, want 1 (default)", got)
|
||||
}
|
||||
// A zero/negative config value is treated as 1.
|
||||
// A positive value is the hard cap.
|
||||
srv.cfg.Snapshots.Limit = 5
|
||||
if got := srv.mgr.SnapshotLimit(); got != 5 {
|
||||
t.Fatalf("SnapshotLimit() with 5 = %d, want 5", got)
|
||||
}
|
||||
// 0 disables snapshots (no new snapshots, existing ones untouched).
|
||||
srv.cfg.Snapshots.Limit = 0
|
||||
if got := srv.mgr.SnapshotLimit(); got != 1 {
|
||||
t.Fatalf("SnapshotLimit() with 0 = %d, want 1", got)
|
||||
if got := srv.mgr.SnapshotLimit(); got != 0 {
|
||||
t.Fatalf("SnapshotLimit() with 0 = %d, want 0 (disabled)", got)
|
||||
}
|
||||
// A negative value (shouldn't pass config validation) is treated as disabled.
|
||||
srv.cfg.Snapshots.Limit = -1
|
||||
if got := srv.mgr.SnapshotLimit(); got != 0 {
|
||||
t.Fatalf("SnapshotLimit() with -1 = %d, want 0 (disabled)", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user