Skip to content

Commit 3697bdc

Browse files
authored
o/assertstate: add Commit to validation-sets confdb handler (#17344)
* o/assertstate: add Commit to validation-sets confdb handler Adds a Commit() method to the validation-sets confdb handler. This enables the handler to convert confdb changes into validation-set metadata changes (mode, pinned-sequence). Signed-off-by: Miguel Pires <miguel.pires@canonical.com>
1 parent 86f49b6 commit 3697bdc

5 files changed

Lines changed: 353 additions & 8 deletions

File tree

overlord/assertstate/confdb/validation_sets.go

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727

2828
"github.com/snapcore/snapd/asserts"
2929
"github.com/snapcore/snapd/asserts/snapasserts"
30-
confdbpkg "github.com/snapcore/snapd/confdb"
30+
"github.com/snapcore/snapd/confdb"
3131
"github.com/snapcore/snapd/overlord/assertstate"
3232
"github.com/snapcore/snapd/overlord/confdbstate"
3333
"github.com/snapcore/snapd/overlord/snapstate"
@@ -226,14 +226,14 @@ func (c *ValsetsConfdbHandler) SchemaName() string {
226226
// Databag reads all validation set tracking from the state and returns a
227227
// confdb.JSONDatabag structured as described in the system/validation-sets
228228
// confdb-schema. State must be locked by caller.
229-
func (c *ValsetsConfdbHandler) Databag(st *state.State) (confdbpkg.JSONDatabag, error) {
229+
func (c *ValsetsConfdbHandler) Databag(st *state.State) (confdb.JSONDatabag, error) {
230230
sets, err := assertstate.ValidationSets(st)
231231
if err != nil {
232232
return nil, err
233233
}
234234

235235
if len(sets) == 0 {
236-
return confdbpkg.NewJSONDatabag(), nil
236+
return confdb.NewJSONDatabag(), nil
237237
}
238238

239239
db := assertstate.DB(st)
@@ -337,6 +337,103 @@ func buildSnapsEntry(snaps []*asserts.ValidationSetSnap) []map[string]any {
337337
return result
338338
}
339339

340-
func (c *ValsetsConfdbHandler) Commit(*state.State, *confdbstate.Transaction) ([]*state.TaskSet, error) {
341-
return nil, errors.New("not implemented yet")
340+
// Commit translates the changes in the Transaction into validation-set state.
341+
// State must be locked by caller.
342+
func (c *ValsetsConfdbHandler) Commit(st *state.State, tx *confdbstate.Transaction) ([]*state.TaskSet, error) {
343+
view, err := confdbstate.GetView(st, "system", "validation-sets", "admin")
344+
if err != nil {
345+
return nil, fmt.Errorf("internal error: unexpected confdb-schema in validation-sets handler: %v", err)
346+
}
347+
348+
type vsKey struct{ account, name string }
349+
valsets := make(map[vsKey][][]confdb.Accessor)
350+
for _, path := range tx.AlteredPaths() {
351+
if len(path) < 3 {
352+
// shouldn't be possible as confdb-schema doesn't allow it
353+
return nil, fmt.Errorf("internal error: unexpected storage path: %v", confdb.JoinAccessors(path))
354+
}
355+
356+
// Databag() will need changes if we add v2 paths in the confdb-schema, so
357+
// fail here to flag the issue
358+
if path[0].Name() != "v1" {
359+
return nil, fmt.Errorf("internal error: cannot write to system/validation-sets: unsupported storage version %q", path[0].Name())
360+
}
361+
362+
k := vsKey{account: path[1].Name(), name: path[2].Name()}
363+
valsets[k] = append(valsets[k], path)
364+
}
365+
366+
for k := range valsets {
367+
request := k.account + "." + k.name
368+
result, err := view.Get(tx, request, nil, confdb.AdminAccess)
369+
if err != nil {
370+
if errors.Is(err, &confdb.NoDataError{}) {
371+
if err := assertstate.ForgetValidationSet(st, k.account, k.name, assertstate.ForgetValidationSetOpts{}); err != nil {
372+
return nil, fmt.Errorf("cannot forget validation set %s/%s: %v", k.account, k.name, err)
373+
}
374+
continue
375+
}
376+
return nil, fmt.Errorf("cannot read validation set %s/%s from confdb: %v", k.account, k.name, err)
377+
}
378+
379+
val, ok := result.(map[string]any)
380+
if !ok {
381+
return nil, fmt.Errorf("internal error: unexpected result type %T for validation set %s/%s", result, k.account, k.name)
382+
}
383+
384+
tr := &assertstate.ValidationSetTracking{}
385+
err = assertstate.GetValidationSet(st, k.account, k.name, tr)
386+
if err != nil && !errors.Is(err, state.ErrNoState) {
387+
return nil, fmt.Errorf("cannot read validation-set %s/%s for commit: %v", k.account, k.name, err)
388+
}
389+
tr.AccountID = k.account
390+
tr.Name = k.name
391+
392+
err = applyChanges(k.account, k.name, tr, val)
393+
if err != nil {
394+
return nil, err
395+
}
396+
397+
assertstate.UpdateValidationSet(st, tr)
398+
}
399+
400+
return nil, nil
401+
}
402+
403+
// applyChanges applies values set through confdb to the ValidationSetTracking.
404+
func applyChanges(accountID, name string, tr *assertstate.ValidationSetTracking, val any) error {
405+
valset, ok := val.(map[string]any)
406+
if !ok {
407+
return fmt.Errorf("internal error: unexpected type %T for validation set %s/%s", val, accountID, name)
408+
}
409+
410+
if rawMode, ok := valset["mode"]; ok {
411+
mode, ok := rawMode.(string)
412+
if !ok {
413+
// writes are validated against the storage schema so shouldn't be possible
414+
return fmt.Errorf(`internal error: "mode" should be a string, got %[1]T: %[1]v`, rawMode)
415+
}
416+
417+
// per the storage schema these are the only choices and it can't be unset
418+
switch mode {
419+
case "monitor":
420+
tr.Mode = assertstate.Monitor
421+
case "enforce":
422+
tr.Mode = assertstate.Enforce
423+
}
424+
}
425+
426+
if rawSeq, ok := valset["pinned-sequence"]; ok {
427+
v, ok := rawSeq.(float64)
428+
if !ok {
429+
// writes are validated against the storage schema so shouldn't be possible
430+
return fmt.Errorf(`internal error: "pinned-sequence" should be an int, got %[1]T: %[1]v`, rawSeq)
431+
}
432+
433+
tr.PinnedAt = int(v)
434+
} else {
435+
tr.PinnedAt = 0
436+
}
437+
438+
return nil
342439
}

overlord/assertstate/confdb/validation_sets_test.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,251 @@ func (s *confdbHandlerSuite) TestDatabagMultipleSetsAndAccounts(c *C) {
276276
},
277277
})
278278
}
279+
280+
func (s *confdbHandlerSuite) TestUpdateEntireValidationSet(c *C) {
281+
s.st.Lock()
282+
defer s.st.Unlock()
283+
284+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
285+
AccountID: "my-account",
286+
Name: "my-set",
287+
Mode: assertstate.Monitor,
288+
Current: 1,
289+
})
290+
291+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
292+
c.Assert(err, IsNil)
293+
294+
// set the entire validation set map
295+
err = s.view.Set(tx, "my-account.my-set", map[string]any{"mode": "enforce", "pinned-sequence": 5})
296+
c.Assert(err, IsNil)
297+
298+
handler := &assertstateconfdb.ValsetsConfdbHandler{}
299+
_, err = handler.Commit(s.st, tx)
300+
c.Assert(err, IsNil)
301+
302+
var tr assertstate.ValidationSetTracking
303+
err = assertstate.GetValidationSet(s.st, "my-account", "my-set", &tr)
304+
c.Assert(err, IsNil)
305+
c.Check(tr.Mode, Equals, assertstate.Enforce)
306+
c.Check(tr.PinnedAt, Equals, 5)
307+
}
308+
309+
func (s *confdbHandlerSuite) TestCommitUpdatesOnlyMode(c *C) {
310+
s.st.Lock()
311+
defer s.st.Unlock()
312+
313+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
314+
AccountID: "my-account",
315+
Name: "my-set",
316+
Mode: assertstate.Enforce,
317+
PinnedAt: 3,
318+
Current: 1,
319+
})
320+
321+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
322+
c.Assert(err, IsNil)
323+
324+
// set specific path and check other data isn't affected
325+
err = s.view.Set(tx, "my-account.my-set.mode", "monitor")
326+
c.Assert(err, IsNil)
327+
328+
handler := &assertstateconfdb.ValsetsConfdbHandler{}
329+
_, err = handler.Commit(s.st, tx)
330+
c.Assert(err, IsNil)
331+
332+
var tr assertstate.ValidationSetTracking
333+
err = assertstate.GetValidationSet(s.st, "my-account", "my-set", &tr)
334+
c.Assert(err, IsNil)
335+
c.Check(tr.Mode, Equals, assertstate.Monitor)
336+
c.Check(tr.PinnedAt, Equals, 3)
337+
338+
// if we set the entire validation set map without pinned-sequence, it's removed
339+
tx, err = confdbstate.NewTransaction(s.st, "system", "validation-sets")
340+
c.Assert(err, IsNil)
341+
342+
err = s.view.Set(tx, "my-account.my-set", map[string]any{"mode": "enforce"})
343+
c.Assert(err, IsNil)
344+
345+
_, err = handler.Commit(s.st, tx)
346+
c.Assert(err, IsNil)
347+
348+
err = assertstate.GetValidationSet(s.st, "my-account", "my-set", &tr)
349+
c.Assert(err, IsNil)
350+
c.Check(tr.Mode, Equals, assertstate.Enforce)
351+
c.Check(tr.PinnedAt, Equals, 0)
352+
}
353+
354+
func (s *confdbHandlerSuite) TestCommitUnsetsPinnedSequence(c *C) {
355+
s.st.Lock()
356+
defer s.st.Unlock()
357+
358+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
359+
AccountID: "my-account",
360+
Name: "my-set",
361+
Mode: assertstate.Enforce,
362+
PinnedAt: 7,
363+
Current: 1,
364+
})
365+
366+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
367+
c.Assert(err, IsNil)
368+
369+
// unset part of the data
370+
err = s.view.Unset(tx, "my-account.my-set.pinned-sequence")
371+
c.Assert(err, IsNil)
372+
373+
handler := &assertstateconfdb.ValsetsConfdbHandler{}
374+
_, err = handler.Commit(s.st, tx)
375+
c.Assert(err, IsNil)
376+
377+
var tr assertstate.ValidationSetTracking
378+
err = assertstate.GetValidationSet(s.st, "my-account", "my-set", &tr)
379+
c.Assert(err, IsNil)
380+
c.Check(tr.Mode, Equals, assertstate.Enforce)
381+
c.Check(tr.PinnedAt, Equals, 0)
382+
}
383+
384+
func (s *confdbHandlerSuite) TestCannotUnsetMode(c *C) {
385+
s.st.Lock()
386+
defer s.st.Unlock()
387+
388+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
389+
AccountID: "my-account",
390+
Name: "my-set",
391+
Mode: assertstate.Enforce,
392+
PinnedAt: 7,
393+
Current: 1,
394+
})
395+
396+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
397+
c.Assert(err, IsNil)
398+
399+
val, err := s.view.Get(tx, "my-account.my-set.mode", nil, confdb.AdminAccess)
400+
c.Assert(err, IsNil)
401+
c.Assert(val, Equals, "enforce")
402+
403+
err = s.view.Set(tx, "my-account.my-set", map[string]any{
404+
"pinned-sequence": 5,
405+
})
406+
c.Assert(err, ErrorMatches, `.*cannot find required combinations of keys`)
407+
408+
// unsetting mode fails because the storage schema marks it as required
409+
err = s.view.Unset(tx, "my-account.my-set.mode")
410+
c.Assert(err, ErrorMatches, `.*cannot find required combinations of keys`)
411+
}
412+
413+
func (s *confdbHandlerSuite) TestCommitForgetsDeletedValidationSet(c *C) {
414+
s.st.Lock()
415+
defer s.st.Unlock()
416+
417+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
418+
AccountID: "my-account",
419+
Name: "my-set",
420+
Mode: assertstate.Monitor,
421+
Current: 1,
422+
})
423+
424+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
425+
c.Assert(err, IsNil)
426+
427+
// unset through confdb
428+
err = s.view.Unset(tx, "my-account.my-set")
429+
c.Assert(err, IsNil)
430+
431+
handler := &assertstateconfdb.ValsetsConfdbHandler{}
432+
_, err = handler.Commit(s.st, tx)
433+
c.Assert(err, IsNil)
434+
435+
// check it was deleted from state
436+
var tr assertstate.ValidationSetTracking
437+
err = assertstate.GetValidationSet(s.st, "my-account", "my-set", &tr)
438+
c.Assert(err, testutil.ErrorIs, state.ErrNoState)
439+
}
440+
441+
func (s *confdbHandlerSuite) TestCommitRejectsUnsupportedStorageVersion(c *C) {
442+
s.st.Lock()
443+
defer s.st.Unlock()
444+
445+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
446+
c.Assert(err, IsNil)
447+
448+
path, err := confdb.ParsePathIntoAccessors("v2.my-account.my-set.mode", confdb.ParseOptions{})
449+
c.Assert(err, IsNil)
450+
err = tx.Set(path, "enforce")
451+
c.Assert(err, IsNil)
452+
453+
handler := &assertstateconfdb.ValsetsConfdbHandler{}
454+
_, err = handler.Commit(s.st, tx)
455+
c.Assert(err, ErrorMatches, `internal error: cannot write to system/validation-sets: unsupported storage version "v2"`)
456+
}
457+
458+
func (s *confdbHandlerSuite) TestCommitMultipleSetsAcrossAccounts(c *C) {
459+
s.st.Lock()
460+
defer s.st.Unlock()
461+
462+
s.addValidationSetAssert(c, "acct1", "set-a", 1, nil)
463+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
464+
AccountID: "acct1",
465+
Name: "set-a",
466+
Mode: assertstate.Monitor,
467+
Current: 1,
468+
})
469+
s.addValidationSetAssert(c, "acct1", "set-b", 1, nil)
470+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
471+
AccountID: "acct1",
472+
Name: "set-b",
473+
Mode: assertstate.Enforce,
474+
PinnedAt: 1,
475+
Current: 1,
476+
})
477+
s.addValidationSetAssert(c, "acct2", "set-c", 1, nil)
478+
assertstate.UpdateValidationSet(s.st, &assertstate.ValidationSetTracking{
479+
AccountID: "acct2",
480+
Name: "set-c",
481+
Mode: assertstate.Enforce,
482+
PinnedAt: 2,
483+
Current: 1,
484+
})
485+
486+
tx, err := confdbstate.NewTransaction(s.st, "system", "validation-sets")
487+
c.Assert(err, IsNil)
488+
489+
err = s.view.Set(tx, "acct1.set-a", map[string]any{
490+
"mode": "enforce",
491+
"pinned-sequence": 10,
492+
})
493+
c.Assert(err, IsNil)
494+
err = s.view.Set(tx, "acct1.set-b", map[string]any{
495+
"mode": "monitor",
496+
"pinned-sequence": 1,
497+
})
498+
c.Assert(err, IsNil)
499+
err = s.view.Set(tx, "acct2.set-c", map[string]any{
500+
"mode": "monitor",
501+
"pinned-sequence": 2,
502+
})
503+
c.Assert(err, IsNil)
504+
505+
handler := &assertstateconfdb.ValsetsConfdbHandler{}
506+
_, err = handler.Commit(s.st, tx)
507+
c.Assert(err, IsNil)
508+
509+
for _, tc := range []struct {
510+
account string
511+
name string
512+
mode assertstate.ValidationSetMode
513+
pin int
514+
}{
515+
{account: "acct1", name: "set-a", mode: assertstate.Enforce, pin: 10},
516+
{account: "acct1", name: "set-b", mode: assertstate.Monitor, pin: 1},
517+
{account: "acct2", name: "set-c", mode: assertstate.Monitor, pin: 2},
518+
} {
519+
var tr assertstate.ValidationSetTracking
520+
err = assertstate.GetValidationSet(s.st, tc.account, tc.name, &tr)
521+
c.Assert(err, IsNil)
522+
c.Check(tr.Mode, Equals, tc.mode)
523+
c.Check(tr.Current, Equals, 1)
524+
c.Check(tr.PinnedAt, Equals, tc.pin)
525+
}
526+
}

overlord/confdbstate/confdbmgr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func (m *ConfdbManager) doCommitTransaction(t *state.Task, _ *tomb.Tomb) (err er
164164
}
165165

166166
view := confdbAssert.Schema().View(viewName)
167-
paths := tx.alteredPaths()
167+
paths := tx.AlteredPaths()
168168
mightAffectEph, err := view.WriteAffectsEphemeral(paths)
169169
if err != nil {
170170
return fmt.Errorf("cannot commit transaction: cannot check for ephemeral paths: %v", err)

0 commit comments

Comments
 (0)