Skip to content

Commit

Permalink
daemon, tests: expose installing offline components in api and test c…
Browse files Browse the repository at this point in the history
…hange
  • Loading branch information
andrewphelpsj committed Feb 4, 2025
1 parent 9395efd commit 455bdc6
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 10 deletions.
45 changes: 42 additions & 3 deletions daemon/api_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,26 +188,65 @@ func startOfflineRemodelChange(st *state.State, newModel *asserts.Model,

*pathsToNotRemove = make([]string, 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, 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, devicestate.RemodelOptions{
// since this is the codepath that parses the form, offline is implicit
// because local snaps are being provided.
Offline: true,
LocalSnaps: localSnaps,
Offline: true,
LocalSnaps: localSnaps,
LocalComponents: localComponents,
})
if err != nil {
return nil, BadRequest("cannot remodel device: %v", err)
Expand Down
151 changes: 151 additions & 0 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 @@ -674,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 @@ -1386,10 +1386,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 @@ -1399,7 +1409,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 @@ -2053,25 +2063,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
Loading

0 comments on commit 455bdc6

Please sign in to comment.