Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

many: handle components during an offline remodel #15009

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
55 changes: 48 additions & 7 deletions daemon/api_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
)

Expand Down Expand Up @@ -117,7 +118,7 @@
st.Lock()
defer st.Unlock()

chg, err := devicestateRemodel(st, newModel, nil, devicestate.RemodelOptions{
chg, err := devicestateRemodel(st, newModel, devicestate.RemodelOptions{
Offline: data.Offline,
})
if err != nil {
Expand Down Expand Up @@ -186,26 +187,66 @@
}

*pathsToNotRemove = make([]string, 0, len(slInfo.snaps))
localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.snaps))
localSnaps := make([]snapstate.PathSnap, 0, len(slInfo.snaps))
localComponents := make([]snapstate.PathComponent, 0)
for _, psi := range slInfo.snaps {
// Move file to the same name of what a downloaded one would have
dest := filepath.Join(dirs.SnapBlobDir,
fmt.Sprintf("%s_%s.snap", psi.info.RealName, psi.info.Revision))
os.Rename(psi.tmpPath, dest)
if err := os.Rename(psi.tmpPath, dest); err != nil {
return nil, InternalError("cannot move uploaded snap file: %v", err)
}

Check warning on line 198 in daemon/api_model.go

View check run for this annotation

Codecov / codecov/patch

daemon/api_model.go#L197-L198

Added lines #L197 - L198 were not covered by tests

// Avoid trying to remove a file that does not exist anymore
*pathsToNotRemove = append(*pathsToNotRemove, psi.tmpPath)

localSnaps = append(localSnaps, devicestate.LocalSnap{
localSnaps = append(localSnaps, snapstate.PathSnap{
SideInfo: &psi.info.SideInfo,
Path: dest,
})

for _, comp := range psi.components {
dest := filepath.Join(dirs.SnapBlobDir,
fmt.Sprintf("%s_%s.comp", comp.sideInfo.Component, comp.sideInfo.Revision))

if err := os.Rename(comp.tmpPath, dest); err != nil {
return nil, InternalError("cannot move uploaded component file: %v", err)
}

Check warning on line 214 in daemon/api_model.go

View check run for this annotation

Codecov / codecov/patch

daemon/api_model.go#L213-L214

Added lines #L213 - L214 were not covered by tests

// Avoid trying to remove a file that does not exist anymore
*pathsToNotRemove = append(*pathsToNotRemove, comp.tmpPath)

localComponents = append(localComponents, snapstate.PathComponent{
SideInfo: comp.sideInfo,
Path: dest,
})
}
}

for _, comp := range slInfo.components {
dest := filepath.Join(dirs.SnapBlobDir,
fmt.Sprintf("%s_%s.comp", comp.sideInfo.Component, comp.sideInfo.Revision))

if err := os.Rename(comp.tmpPath, dest); err != nil {
return nil, InternalError("cannot move uploaded component file: %v", err)
}

Check warning on line 232 in daemon/api_model.go

View check run for this annotation

Codecov / codecov/patch

daemon/api_model.go#L231-L232

Added lines #L231 - L232 were not covered by tests

// Avoid trying to remove a file that does not exist anymore
*pathsToNotRemove = append(*pathsToNotRemove, comp.tmpPath)

localComponents = append(localComponents, snapstate.PathComponent{
SideInfo: comp.sideInfo,
Path: dest,
})
}

// Now create and start the remodel change
chg, err := devicestateRemodel(st, newModel, localSnaps, devicestate.RemodelOptions{
// since this is the codepath that parses the form, offline is implcit
chg, err := devicestateRemodel(st, newModel, devicestate.RemodelOptions{
// since this is the codepath that parses the form, offline is implicit
// because local snaps are being provided.
Offline: true,
Offline: true,
LocalSnaps: localSnaps,
LocalComponents: localComponents,
})
if err != nil {
return nil, BadRequest("cannot remodel device: %v", err)
Expand Down
164 changes: 157 additions & 7 deletions daemon/api_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"time"
Expand All @@ -39,13 +41,17 @@ import (
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/client/clientutil"
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
)

var modelDefaults = map[string]interface{}{
Expand Down Expand Up @@ -133,7 +139,7 @@ func (s *modelSuite) testPostRemodel(c *check.C, offline bool) {
defer restore()

var devicestateRemodelGotModel *asserts.Model
defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, localSnaps []devicestate.LocalSnap, opts devicestate.RemodelOptions) (*state.Change, error) {
defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, opts devicestate.RemodelOptions) (*state.Change, error) {
c.Check(opts.Offline, check.Equals, offline)
devicestateRemodelGotModel = nm
chg := st.NewChange("remodel", "...")
Expand Down Expand Up @@ -599,13 +605,12 @@ func (s *modelSuite) testPostOfflineRemodel(c *check.C, params *testPostOfflineR
snapName := "snap1"
snapRev := 1001
var devicestateRemodelGotModel *asserts.Model
defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model,
localSnaps []devicestate.LocalSnap, opts devicestate.RemodelOptions) (*state.Change, error) {
defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, opts devicestate.RemodelOptions) (*state.Change, error) {
c.Check(opts.Offline, check.Equals, true)
c.Check(len(localSnaps), check.Equals, 1)
c.Check(localSnaps[0].SideInfo.RealName, check.Equals, snapName)
c.Check(localSnaps[0].SideInfo.Revision, check.Equals, snap.Revision{N: snapRev})
c.Check(strings.HasSuffix(localSnaps[0].Path,
c.Check(len(opts.LocalSnaps), check.Equals, 1)
c.Check(opts.LocalSnaps[0].SideInfo.RealName, check.Equals, snapName)
c.Check(opts.LocalSnaps[0].SideInfo.Revision, check.Equals, snap.Revision{N: snapRev})
c.Check(strings.HasSuffix(opts.LocalSnaps[0].Path,
"/var/lib/snapd/snaps/"+snapName+"_"+strconv.Itoa(snapRev)+".snap"),
check.Equals, true)

Expand Down Expand Up @@ -675,3 +680,148 @@ func (s *modelSuite) testPostOfflineRemodel(c *check.C, params *testPostOfflineR
c.Assert(soon, check.Equals, 1)
}
}

func (s *modelSuite) TestPostOfflineRemodelWithComponents(c *check.C) {
s.expectRootAccess()

oldModel := s.Brands.Model("my-brand", "my-old-model", modelDefaults)
newModel := s.Brands.Model("my-brand", "my-old-model", modelDefaults, map[string]interface{}{
"revision": "2",
})

d := s.daemonWithOverlordMockAndStore()
hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
c.Assert(err, check.IsNil)
deviceMgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
c.Assert(err, check.IsNil)
d.Overlord().AddManager(deviceMgr)

st := d.Overlord().State()
st.Lock()
defer st.Unlock()

st.Set("seeded", true)

assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""))
assertstatetest.AddMany(st, s.Brands.AccountsAndKeys("my-brand")...)
s.mockModel(st, oldModel)

signer := assertstest.NewStoreStack("can0nical", nil)
assertstatetest.AddMany(st, signer.StoreAccountKey(""))

account := assertstest.NewAccount(signer, "developer1", nil, "")
c.Assert(signer.Add(account), check.IsNil)

const snapName = "some-snap"
snapID := snaptest.AssertedSnapID(snapName)
snapFormData := make(map[string]string)

snapPath := snaptest.MakeTestSnapWithFiles(c, withComponents("name: some-snap\nversion: 1", []string{"comp"}), nil)
digest, size, err := asserts.SnapFileSHA3_384(snapPath)
c.Assert(err, check.IsNil)

content, err := os.ReadFile(snapPath)
c.Assert(err, check.IsNil)
snapFormData[filepath.Base(snapPath)] = string(content)

rev := mockStoreAssertion(c, signer, signer.AuthorityID, account.AccountID(), asserts.SnapRevisionType, map[string]interface{}{
"snap-id": snapID,
"snap-sha3-384": digest,
"developer-id": account.AccountID(),
"snap-size": strconv.Itoa(int(size)),
"snap-revision": "10",
})

decl := mockStoreAssertion(c, signer, signer.AuthorityID, account.AccountID(), asserts.SnapDeclarationType, map[string]interface{}{
"series": "16",
"snap-id": snapID,
"snap-name": snapName,
"publisher-id": account.AccountID(),
})

compPath, resRev, resPair := makeStandardComponent(c, signer, signer.AuthorityID, account.AccountID(), snapName, "comp")
content, err = os.ReadFile(compPath)
c.Assert(err, check.IsNil)
snapFormData[filepath.Base(compPath)] = string(content)

// we handle components that are not associated with any of the snaps that
// are being uploaded a little differently, this part of the test helps
// cover that case
extraSnapDecl := mockStoreAssertion(c, signer, signer.AuthorityID, account.AccountID(), asserts.SnapDeclarationType, map[string]interface{}{
"series": "16",
"snap-id": snaptest.AssertedSnapID("other-snap"),
"snap-name": "other-snap",
"publisher-id": account.AccountID(),
})
extraCompPath, extraResRev, extraResPair := makeStandardComponent(c, signer, signer.AuthorityID, account.AccountID(), "other-snap", "comp")
content, err = os.ReadFile(extraCompPath)
c.Assert(err, check.IsNil)
snapFormData[filepath.Base(extraCompPath)] = string(content)

snapstate.Set(st, "other-snap", &snapstate.SnapState{
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{
RealName: "other-snap",
Revision: snap.R(10),
SnapID: snaptest.AssertedSnapID("other-snap"),
}}),
Current: snap.R(10),
Active: true,
})

var assertions strings.Builder
encoder := asserts.NewEncoder(&assertions)

for _, a := range []asserts.Assertion{rev, decl, resRev, resPair, extraSnapDecl, extraResRev, extraResPair, account} {
err := encoder.Encode(a)
c.Assert(err, check.IsNil)
}

fields := map[string][]string{
"new-model": {string(asserts.Encode(newModel))},
"assertion": {assertions.String()},
}
form, boundary := createFormData(c, fields, snapFormData)

defer daemon.MockDevicestateRemodel(func(st *state.State, nm *asserts.Model, opts devicestate.RemodelOptions) (*state.Change, error) {
c.Check(opts.Offline, check.Equals, true)
c.Assert(len(opts.LocalSnaps), check.Equals, 1)
c.Check(opts.LocalSnaps[0].SideInfo.RealName, check.Equals, snapName)
c.Check(opts.LocalSnaps[0].SideInfo.Revision, check.Equals, snap.Revision{N: 10})

snapPath := filepath.Join(dirs.SnapBlobDir, "some-snap_10.snap")
c.Check(strings.HasSuffix(opts.LocalSnaps[0].Path, snapPath), check.Equals, true)

c.Assert(len(opts.LocalComponents), check.Equals, 2)
compPath := filepath.Join(dirs.SnapBlobDir, "some-snap+comp_20.comp")
c.Check(strings.HasSuffix(opts.LocalComponents[0].Path, compPath), check.Equals, true)

extraCompPath := filepath.Join(dirs.SnapBlobDir, "other-snap+comp_20.comp")
c.Check(strings.HasSuffix(opts.LocalComponents[1].Path, extraCompPath), check.Equals, true)

c.Check(nm, check.DeepEquals, newModel)

chg := st.NewChange("remodel", "...")
return chg, nil
})()

req, err := http.NewRequest("POST", "/v2/model", &form)
c.Assert(err, check.IsNil)
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
req.Header.Set("Content-Length", strconv.Itoa(form.Len()))

st.Unlock()
rsp := s.asyncReq(c, req, nil)
st.Lock()

c.Assert(rsp.Status, check.Equals, 202)
c.Check(rsp.Change, check.DeepEquals, "1")

chg := st.Change(rsp.Change)
c.Assert(chg, check.NotNil)

c.Assert(st.Changes(), check.HasLen, 1)
chg1 := st.Changes()[0]
c.Assert(chg, check.DeepEquals, chg1)
c.Assert(chg.Kind(), check.Equals, "remodel")
c.Assert(chg.Err(), check.IsNil)
}
35 changes: 28 additions & 7 deletions daemon/api_systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1422,10 +1422,20 @@ func (s *systemsCreateSuite) mockDevAssertion(c *check.C, t *asserts.AssertionTy
}

func (s *systemsCreateSuite) mockStoreAssertion(c *check.C, t *asserts.AssertionType, extras map[string]interface{}) asserts.Assertion {
return mockStoreAssertion(c, s.storeSigning, s.storeSigning.AuthorityID, s.dev1acct.AccountID(), t, extras)
}

func mockStoreAssertion(
c *check.C,
signer assertstest.SignerDB,
authorityID, accountID string,
t *asserts.AssertionType,
extras map[string]interface{},
) asserts.Assertion {
headers := map[string]interface{}{
"type": t.Name,
"authority-id": s.storeSigning.AuthorityID,
"account-id": s.dev1acct.AccountID(),
"authority-id": authorityID,
"account-id": accountID,
"series": "16",
"revision": "5",
"timestamp": "2030-11-06T09:16:26Z",
Expand All @@ -1435,7 +1445,7 @@ func (s *systemsCreateSuite) mockStoreAssertion(c *check.C, t *asserts.Assertion
headers[k] = v
}

vs, err := s.storeSigning.Sign(t, headers, nil, "")
vs, err := signer.Sign(t, headers, nil, "")
c.Assert(err, check.IsNil)
return vs
}
Expand Down Expand Up @@ -2089,25 +2099,36 @@ func (s *systemsCreateSuite) TestCreateSystemActionWithComponentsOffline(c *chec
}

func (s *systemsCreateSuite) makeStandardComponent(c *check.C, snapName string, compName string) (compPath string, resourceRev, resourcePair asserts.Assertion) {
return makeStandardComponent(c, s.storeSigning, s.storeSigning.AuthorityID, s.dev1acct.AccountID(), snapName, compName)
}

func makeStandardComponent(
c *check.C,
signer assertstest.SignerDB,
authorityID string,
accountID string,
snapName string,
compName string,
) (compPath string, resourceRev, resourcePair asserts.Assertion) {
yaml := fmt.Sprintf("component: %s+%s\nversion: 1\ntype: standard", snapName, compName)
compPath = snaptest.MakeTestComponent(c, yaml)

digest, size, err := asserts.SnapFileSHA3_384(compPath)
c.Assert(err, check.IsNil)

resRev := s.mockStoreAssertion(c, asserts.SnapResourceRevisionType, map[string]interface{}{
resRev := mockStoreAssertion(c, signer, authorityID, accountID, asserts.SnapResourceRevisionType, map[string]interface{}{
"snap-id": snaptest.AssertedSnapID(snapName),
"developer-id": s.dev1acct.AccountID(),
"developer-id": accountID,
"resource-name": compName,
"resource-sha3-384": digest,
"resource-revision": "20",
"resource-size": strconv.Itoa(int(size)),
"timestamp": time.Now().Format(time.RFC3339),
})

resPair := s.mockStoreAssertion(c, asserts.SnapResourcePairType, map[string]interface{}{
resPair := mockStoreAssertion(c, signer, authorityID, accountID, asserts.SnapResourcePairType, map[string]interface{}{
"snap-id": snaptest.AssertedSnapID(snapName),
"developer-id": s.dev1acct.AccountID(),
"developer-id": accountID,
"resource-name": compName,
"resource-revision": "20",
"snap-revision": "10",
Expand Down
2 changes: 1 addition & 1 deletion daemon/export_api_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/snapcore/snapd/overlord/state"
)

func MockDevicestateRemodel(mock func(*state.State, *asserts.Model, []devicestate.LocalSnap, devicestate.RemodelOptions) (*state.Change, error)) (restore func()) {
func MockDevicestateRemodel(mock func(*state.State, *asserts.Model, devicestate.RemodelOptions) (*state.Change, error)) (restore func()) {
oldDevicestateRemodel := devicestateRemodel
devicestateRemodel = mock
return func() {
Expand Down
Loading
Loading