Skip to content

Commit

Permalink
o/snapstate, overlord, o/devicestate: support downloading components …
Browse files Browse the repository at this point in the history
…in snapstate.Download
  • Loading branch information
andrewphelpsj committed Dec 10, 2024
1 parent d5efb26 commit 8b8a47e
Show file tree
Hide file tree
Showing 8 changed files with 754 additions and 121 deletions.
5 changes: 2 additions & 3 deletions overlord/devicestate/devicestate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1764,15 +1764,14 @@ func CreateRecoverySystem(st *state.State, label string, opts CreateRecoverySyst
continue
}

const userID = 0
// TODO: this respects the passed in validation sets, but does not
// currently respect refresh-control style of constraining snap
// revisions.
ts, info, err := snapstateDownload(context.TODO(), st, sn.Name, dirs.SnapBlobDir, &snapstate.RevisionOptions{
ts, info, err := snapstateDownload(context.TODO(), st, sn.Name, nil, dirs.SnapBlobDir, snapstate.RevisionOptions{
Channel: sn.DefaultChannel,
Revision: rev,
ValidationSets: valsets,
}, userID, snapstate.Flags{}, nil)
}, snapstate.Options{})
if err != nil {
return nil, err
}
Expand Down
20 changes: 10 additions & 10 deletions overlord/devicestate/devicestate_systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3465,20 +3465,20 @@ func (s *deviceMgrSystemsCreateSuite) testDeviceManagerCreateRecoverySystemValid
}, nil)

devicestate.MockSnapstateDownload(func(
_ context.Context, _ *state.State, name string, _ string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, _ snapstate.DeviceContext) (*state.TaskSet, *snap.Info, error,
ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error,
) {
expectedRev, ok := snapRevisions[name]
if !ok {
return nil, nil, fmt.Errorf("unexpected snap name %q", name)
}

c.Check(expectedRev, Equals, opts.Revision)
c.Check(expectedRev, Equals, revOpts.Revision)

tDownload := s.state.NewTask("mock-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel))
tDownload := s.state.NewTask("mock-download", fmt.Sprintf("Download %s to track %s", name, revOpts.Channel))

si := &snap.SideInfo{
RealName: name,
Revision: opts.Revision,
Revision: revOpts.Revision,
SnapID: fakeSnapID(name),
}
tDownload.Set("snap-setup", &snapstate.SnapSetup{
Expand Down Expand Up @@ -3654,7 +3654,7 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemOffli
defer s.state.Unlock()

devicestate.MockSnapstateDownload(func(
_ context.Context, _ *state.State, name string, _ string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, _ snapstate.DeviceContext) (*state.TaskSet, *snap.Info, error,
ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error,
) {
c.Errorf("snapstate.Download called unexpectedly")
return nil, nil, nil
Expand Down Expand Up @@ -3825,7 +3825,7 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValid
assertstatetest.AddMany(s.state, vsetAssert)

devicestate.MockSnapstateDownload(func(
_ context.Context, _ *state.State, name string, _ string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, _ snapstate.DeviceContext) (*state.TaskSet, *snap.Info, error,
ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error,
) {
c.Errorf("snapstate.Download called unexpectedly")
return nil, nil, nil
Expand Down Expand Up @@ -4310,19 +4310,19 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValid
vset := vsetAssert.(*asserts.ValidationSet)

devicestate.MockSnapstateDownload(func(
_ context.Context, _ *state.State, name string, _ string, opts *snapstate.RevisionOptions, _ int, _ snapstate.Flags, _ snapstate.DeviceContext) (*state.TaskSet, *snap.Info, error,
ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error,
) {
expectedRev, ok := snapRevisions[name]
if !ok {
return nil, nil, fmt.Errorf("unexpected snap name %q", name)
}

c.Check(expectedRev, Equals, opts.Revision)
c.Check(expectedRev, Equals, revOpts.Revision)

tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel))
tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, revOpts.Channel))
si := &snap.SideInfo{
RealName: name,
Revision: opts.Revision,
Revision: revOpts.Revision,
SnapID: fakeSnapID(name),
}

Expand Down
2 changes: 1 addition & 1 deletion overlord/devicestate/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func MockSnapstateUpdatePathWithDeviceContext(f func(st *state.State, si *snap.S
return r
}

func MockSnapstateDownload(f func(ctx context.Context, st *state.State, name string, blobDirectory string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, *snap.Info, error)) (restore func()) {
func MockSnapstateDownload(f func(ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error)) (restore func()) {
r := testutil.Backup(&snapstateDownload)
snapstateDownload = f
return r
Expand Down
6 changes: 3 additions & 3 deletions overlord/managers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13693,7 +13693,7 @@ func (s *mgrsSuite) testDownload(c *C, downloadDir string) {
st.Lock()
defer st.Unlock()

ts, info, err := snapstate.Download(context.TODO(), st, "foo", downloadDir, nil, 0, snapstate.Flags{}, nil)
ts, info, err := snapstate.Download(context.TODO(), st, "foo", nil, downloadDir, snapstate.RevisionOptions{}, snapstate.Options{})
c.Assert(err, IsNil)
chg := st.NewChange("download-snap", "...")
chg.AddAll(ts)
Expand Down Expand Up @@ -13753,9 +13753,9 @@ func (s *mgrsSuite) TestDownloadSpecificRevision(c *C) {
st.Lock()
defer st.Unlock()

ts, info, err := snapstate.Download(context.TODO(), st, "foo", "", &snapstate.RevisionOptions{
ts, info, err := snapstate.Download(context.TODO(), st, "foo", nil, "", snapstate.RevisionOptions{
Revision: snap.R(snapOldRev),
}, 0, snapstate.Flags{}, nil)
}, snapstate.Options{})
c.Assert(err, IsNil)
chg := st.NewChange("download-snap", "...")
chg.AddAll(ts)
Expand Down
157 changes: 125 additions & 32 deletions overlord/snapstate/snapstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1535,22 +1535,30 @@ func InstallPathWithDeviceContext(st *state.State, si *snap.SideInfo, path, name
return ts, nil
}

// Download returns a set of tasks for downloading a snap into the given
// blobDirectory. If blobDirectory is empty, then dirs.SnapBlobDir is used. The
// snap.Info for the snap that is downloaded is also returned. The tasks that
// are returned will also download and validate the snap's assertion.
// Prerequisites for the snap are not downloaded.
func Download(ctx context.Context, st *state.State, name string, blobDirectory string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*state.TaskSet, *snap.Info, error) {
if opts == nil {
opts = &RevisionOptions{}
// Download returns a set of tasks for downloading a snap and components into
// the given blobDirectory. If blobDirectory is empty, then dirs.SnapBlobDir is
// used. The snap.Info for the snap that is downloaded is also returned. The
// tasks that are returned will also download and validate the snap's and
// components' assertions. Prerequisites for the snap are not downloaded.
func Download(
ctx context.Context,
st *state.State,
name string,
components []string,
blobDirectory string,
revOpts RevisionOptions,
opts Options,
) (*state.TaskSet, *snap.Info, error) {
if revOpts.CohortKey != "" && !revOpts.Revision.Unset() {
return nil, nil, errors.New("internal error: cannot specify revision and cohort")

Check warning on line 1553 in overlord/snapstate/snapstate.go

View check run for this annotation

Codecov / codecov/patch

overlord/snapstate/snapstate.go#L1553

Added line #L1553 was not covered by tests
}

if opts.CohortKey != "" && !opts.Revision.Unset() {
return nil, nil, errors.New("cannot specify revision and cohort")
if revOpts.Channel == "" {
revOpts.Channel = "stable"
}

if opts.Channel == "" {
opts.Channel = "stable"
if revOpts.ValidationSets == nil {
revOpts.ValidationSets = snapasserts.NewValidationSets()
}

var snapst SnapState
Expand All @@ -1563,35 +1571,40 @@ func Download(ctx context.Context, st *state.State, name string, blobDirectory s
return nil, nil, fmt.Errorf("invalid instance name: %v", err)
}

sar, err := downloadInfo(ctx, st, name, opts, userID, deviceCtx)
sars, err := sendDownloadActions(ctx, st, []StoreSnap{{
InstanceName: name,
Components: components,
RevOpts: revOpts,
}}, opts)
if err != nil {
return nil, nil, err
}
if len(sars) != 1 {
return nil, nil, fmt.Errorf("expected exactly one result from the store, got %d", len(sars))
}

Check warning on line 1584 in overlord/snapstate/snapstate.go

View check run for this annotation

Codecov / codecov/patch

overlord/snapstate/snapstate.go#L1583-L1584

Added lines #L1583 - L1584 were not covered by tests

sar := sars[0]
info := sar.Info

// if we are going to use the default download dir, and the same snap
// revision is already installed, then we should not overwrite the snap that
// is already in the dir.
if (blobDirectory == "" || blobDirectory == dirs.SnapBlobDir) && info.Revision == snapst.Current {
return nil, nil, &snap.AlreadyInstalledError{Snap: name}
if opts.PrereqTracker != nil {
opts.PrereqTracker.Add(info)
}

if flags.RequireTypeBase && info.Type() != snap.TypeBase && info.Type() != snap.TypeOS {
if opts.Flags.RequireTypeBase && info.Type() != snap.TypeBase && info.Type() != snap.TypeOS {
return nil, nil, fmt.Errorf("unexpected snap type %q, instead of 'base'", info.Type())
}

snapsup := &SnapSetup{
Channel: opts.Channel,
Channel: revOpts.Channel,
Base: info.Base,
UserID: userID,
Flags: flags.ForSnapSetup(),
UserID: opts.UserID,
Flags: opts.Flags.ForSnapSetup(),
DownloadInfo: &info.DownloadInfo,
SideInfo: &info.SideInfo,
Type: info.Type(),
Version: info.Version,
InstanceKey: info.InstanceKey,
CohortKey: opts.CohortKey,
CohortKey: revOpts.CohortKey,
ExpectedProvenance: info.SnapProvenance,
DownloadBlobDir: blobDirectory,
}
Expand All @@ -1600,25 +1613,105 @@ func Download(ctx context.Context, st *state.State, name string, blobDirectory s
snapsup.Channel = sar.RedirectChannel
}

compsups, err := componentTargetsFromActionResult("download", sar, components)
if err != nil {
return nil, nil, fmt.Errorf("cannot extract components from snap resources: %w", err)
}

Check warning on line 1619 in overlord/snapstate/snapstate.go

View check run for this annotation

Codecov / codecov/patch

overlord/snapstate/snapstate.go#L1618-L1619

Added lines #L1618 - L1619 were not covered by tests

for i := range compsups {
compsups[i].DownloadBlobDir = blobDirectory
}

if err := checkSnapActionAgainstValidationSets(sar, compsups, "download", revOpts.ValidationSets); err != nil {
return nil, nil, err
}

toDownloadTo := filepath.Dir(snapsup.MountFile())

// TODO:COMPS: support checking for available space for components
if err := checkDiskSpaceDownload([]minimalInstallInfo{installSnapInfo{info}}, toDownloadTo); err != nil {
return nil, nil, err
}

snapAlreadyInstalled := (blobDirectory == "" || blobDirectory == dirs.SnapBlobDir) &&
snapst.Sequence.LastIndex(info.Revision) != -1
componentAlreadyInstalled := make(map[string]bool, len(compsups))
allInstalled := snapAlreadyInstalled
for _, c := range compsups {
componentAlreadyInstalled[c.ComponentName()] = (blobDirectory == "" || blobDirectory == dirs.SnapBlobDir) &&
snapst.Sequence.IsComponentRevPresent(c.CompSideInfo)
allInstalled = allInstalled && componentAlreadyInstalled[c.ComponentName()]
}

if allInstalled {
return nil, nil, &snap.AlreadyInstalledError{Snap: name}
}

ts := state.NewTaskSet()
var snapsupTask, prev *state.Task
addTask := func(t *state.Task) {
ts.AddTask(t)
if prev == nil {
t.Set("snap-setup", snapsup)
snapsupTask = t
ts.MarkEdge(t, BeginEdge)
} else {
t.WaitFor(prev)
t.Set("snap-setup-task", snapsupTask.ID())
}
prev = t
}

revisionStr := fmt.Sprintf(" (%s)", snapsup.Revision())

download := st.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q%s from channel %q"), snapsup.InstanceName(), revisionStr, snapsup.Channel))
download.Set("snap-setup", snapsup)
if !snapAlreadyInstalled {
download := st.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q%s from channel %q"), snapsup.InstanceName(), revisionStr, snapsup.Channel))
addTask(download)
} else {
// validate-snap expects this to be set, and since we know that this
// already should exist, we can set it here
snapsup.SnapPath = snapsup.MountFile()
}

validate := st.NewTask("validate-snap", fmt.Sprintf(i18n.G("Fetch and check assertions for snap %q%s"), snapsup.InstanceName(), revisionStr))
addTask(validate)

checkAsserts := st.NewTask("validate-snap", fmt.Sprintf(i18n.G("Fetch and check assertions for snap %q%s"), snapsup.InstanceName(), revisionStr))
checkAsserts.Set("snap-setup-task", download.ID())
checkAsserts.WaitFor(download)
compsupIDs := make([]string, 0, len(compsups))
for _, c := range compsups {
rev := fmt.Sprintf(" (%s)", c.CompSideInfo.Revision)

installSet := state.NewTaskSet(download, checkAsserts)
installSet.MarkEdge(download, BeginEdge)
installSet.MarkEdge(checkAsserts, LastBeforeLocalModificationsEdge)
var compsupTaskID string
if !componentAlreadyInstalled[c.ComponentName()] {
download := st.NewTask("download-component", fmt.Sprintf(i18n.G("Download component %q%s"), c.ComponentName(), rev))
download.Set("component-setup", c)
addTask(download)
compsupTaskID = download.ID()
}

// even if the component itself is already installed, it might not have
// been installed with the same snap revision. in that case,
// validate-component will fetch new assertions from the store.
validate := st.NewTask("validate-component", fmt.Sprintf(
i18n.G("Fetch and check assertions for component %q%s"), c.ComponentName(), rev),
)
if compsupTaskID == "" {
validate.Set("component-setup", c)
compsupTaskID = validate.ID()
} else {
validate.Set("component-setup-task", compsupTaskID)
}
addTask(validate)

return installSet, info, nil
compsupIDs = append(compsupIDs, compsupTaskID)
}

snapsupTask.Set("component-setup-tasks", compsupIDs)

// since nothing in this function does any "local" modifications, we just
// set this edge on the last task in the chain
ts.MarkEdge(prev, LastBeforeLocalModificationsEdge)

return ts, info, nil
}

func validatedInfoFromPathAndSideInfo(instanceName string, path string, si *snap.SideInfo) (*snap.Info, error) {
Expand Down
Loading

0 comments on commit 8b8a47e

Please sign in to comment.