Remodeling, DeviceCtx refactoring and new test helpers


#1

During a remodel operation we need to take care of the fact that there is the original model and there is the new model to-be which will be committed only if the entire operation is successful.

The remodel change itself should use the model to-be, while the rest of the system, though for safety we will prevent most other operation to start (through the conflict mechanisms), for correctness should still be governed/use the current model.

To implement this we have and are in the process to remove any naive mapping from the state to the model or aspects tied to the model (gadget, brand store…).

Instead we introduced the concept of snapstate.DeviceContext:

type DeviceContext interface {
	// Model returns the governing device model assertion for the context.
	Model() *asserts.Model
	// Store returns the store service to use under this context or nil if the snapstate store is appropriate.
	Store() StoreService

	// ForRemodeling returns whether this context is for use over a remodeling.
	ForRemodeling() bool
}
    A DeviceContext provides for operating as a given device and with its brand
    store either for normal operation or over a remodeling.

There are helpers to get the right DeviceContext depending on the code situation, and whether a context is already provided. For situations outside a task there are in snapstate:

func DevicePastSeeding(st *state.State, providedDeviceCtx DeviceContext) (DeviceContext, error)
    DevicePastSeeding returns a device context if a model assertion is available
    and the device is seeded, at that point the device store is known and
    seeding done. Otherwise it returns a ChangeConflictError about being too
    early unless a pre-provided DeviceContext is passed in. It will again return
    a conflict error during remodeling unless the providedDeviceCtx is for it.

func DeviceCtxFromState(st *state.State, providedDeviceCtx DeviceContext) (DeviceContext, error)

DeviceCtxFromState exists for operations that do not interact with the store, are used to setup seeding itself.

For the situations inside a task there is snapstate.DeviceCtx which is a hook set to (from devicestate):

func DeviceCtx(st *state.State, task *state.Task, providedDeviceCtx snapstate.DeviceContext) (snapstate.DeviceContext, error)
    DeviceCtx picks a device context from state, optional task or an optionally
    pre-provided one. Returns ErrNoState if a model assertion is not yet known.

ATM the store service to use needs also be accessed in a similar way, using from snapstate:

func Store(st *state.State, deviceCtx DeviceContext) StoreService
    Store returns the store service provided by the optional device context or
    the one used by the snapstate package if the former has no override.

(If all store.Store methods take a context.Context we could/should be able to avoid this indirection and go back to a single snapstate.StoreService).

Related to all this, we have grown tests helpers:

package snapstatetest // import "github.com/snapcore/snapd/overlord/snapstate/snapstatetest"

func MockDeviceContext(deviceCtx snapstate.DeviceContext) (restore func())
func MockDeviceModel(model *asserts.Model) (restore func())
func ReplaceDeviceCtxHook(...) (restore func())
func ReplaceRemodelingHook(remodelingHook func(st *state.State) bool) (restore func())
func UseFallbackDeviceModel() (restore func())
type TrivialDeviceContext struct{ ... }

For places outside of overlord that care about the current model and serial and device information devicestate.(Set)Device|Serial do not exist anymore but they can use:

func (m *DeviceManager) Model() (*asserts.Model, error)
func (m *DeviceManager) Serial() (*asserts.Serial, error

For tests there are now devicestatetest.Device/SetDevice.

During the work for remodeling various other test helpers were also introduced:

  • assertstest.FakeAssertion|FakeAssertionWithBody
  • assertstest.AddMany
  • the assertstest.SigningAccounts struct
  • assertstatetest.AddMany