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

o/snapstate: move auxinfo functions to snapstate backend with metadata helpers #15051

Merged
merged 14 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018-2022 Canonical Ltd
* Copyright (C) 2018-2025 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand All @@ -17,7 +17,7 @@
*
*/

package snapstate
package backend

import (
"encoding/json"
Expand All @@ -30,27 +30,27 @@ import (
"github.com/snapcore/snapd/snap"
)

// auxStoreInfo is information about a snap (*not* a snap revision), not
// AuxStoreInfo is information about a snap (*not* a snap revision), not
// needed in the state, that may be stored to augment the information
// returned for locally-installed snaps
type auxStoreInfo struct {
type AuxStoreInfo struct {
Media snap.MediaInfos `json:"media,omitempty"`
StoreURL string `json:"store-url,omitempty"`
// XXX this is now included in snap.SideInfo.EditedLinks but
// continue having this to support old snapd
Website string `json:"website,omitempty"`
}

func auxStoreInfoFilename(snapID string) string {
func AuxStoreInfoFilename(snapID string) string {
return filepath.Join(dirs.SnapAuxStoreInfoDir, snapID) + ".json"
}

// retrieveAuxStoreInfo loads the stored per-snap auxiliary store info into the given *snap.Info
func retrieveAuxStoreInfo(info *snap.Info) error {
// RetrieveAuxStoreInfo loads the stored per-snap auxiliary store info into the given *snap.Info
func RetrieveAuxStoreInfo(info *snap.Info) error {
if info.SnapID == "" {
return nil
}
f, err := os.Open(auxStoreInfoFilename(info.SnapID))
f, err := os.Open(AuxStoreInfoFilename(info.SnapID))
if err != nil {
if os.IsNotExist(err) {
return nil
Expand All @@ -59,7 +59,7 @@ func retrieveAuxStoreInfo(info *snap.Info) error {
}
defer f.Close()

var aux auxStoreInfo
var aux AuxStoreInfo
dec := json.NewDecoder(f)
if err := dec.Decode(&aux); err != nil {
return fmt.Errorf("cannot decode auxiliary store info for snap %q: %v", info.InstanceName(), err)
Expand All @@ -79,15 +79,15 @@ func retrieveAuxStoreInfo(info *snap.Info) error {
}

// keepAuxStoreInfo saves the given auxiliary store info to disk.
func keepAuxStoreInfo(snapID string, aux *auxStoreInfo) error {
func keepAuxStoreInfo(snapID string, aux *AuxStoreInfo) error {
if snapID == "" {
return nil
}
if err := os.MkdirAll(dirs.SnapAuxStoreInfoDir, 0755); err != nil {
return fmt.Errorf("cannot create directory for auxiliary store info: %v", err)
}

af, err := osutil.NewAtomicFile(auxStoreInfoFilename(snapID), 0644, 0, osutil.NoChown, osutil.NoChown)
af, err := osutil.NewAtomicFile(AuxStoreInfoFilename(snapID), 0644, 0, osutil.NoChown, osutil.NoChown)
if err != nil {
return fmt.Errorf("cannot create file for auxiliary store info for snap %s: %v", snapID, err)
}
Expand All @@ -109,7 +109,7 @@ func discardAuxStoreInfo(snapID string) error {
if snapID == "" {
return nil
}
if err := os.Remove(auxStoreInfoFilename(snapID)); err != nil && !os.IsNotExist(err) {
if err := os.Remove(AuxStoreInfoFilename(snapID)); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("cannot remove auxiliary store info file for snap %s: %v", snapID, err)
}
return nil
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018-2022 Canonical Ltd
* Copyright (C) 2018-2025 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand All @@ -17,7 +17,7 @@
*
*/

package snapstate_test
package backend_test

import (
"path/filepath"
Expand All @@ -26,7 +26,7 @@ import (

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/snap"
)

Expand All @@ -40,30 +40,30 @@ func (s *auxInfoSuite) SetUpTest(c *check.C) {

func (s *auxInfoSuite) TestAuxStoreInfoFilename(c *check.C) {
// precondition check
filename := snapstate.AuxStoreInfoFilename("some-snap-id")
filename := backend.AuxStoreInfoFilename("some-snap-id")
c.Check(filename, check.Equals, filepath.Join(dirs.SnapAuxStoreInfoDir, "some-snap-id.json"))
}

func (s *auxInfoSuite) TestAuxStoreInfoRoundTrip(c *check.C) {
media := snap.MediaInfos{{Type: "1-2-3-testing"}}
info := &snap.Info{SuggestedName: "some-snap"}
info.SnapID = "some-id"
filename := snapstate.AuxStoreInfoFilename(info.SnapID)
filename := backend.AuxStoreInfoFilename(info.SnapID)
c.Assert(osutil.FileExists(filename), check.Equals, false)
c.Check(snapstate.RetrieveAuxStoreInfo(info), check.IsNil)
c.Check(backend.RetrieveAuxStoreInfo(info), check.IsNil)
c.Check(info.Media, check.HasLen, 0)
c.Check(info.Website(), check.Equals, "")
c.Check(info.StoreURL, check.Equals, "")

aux := &snapstate.AuxStoreInfo{
aux := &backend.AuxStoreInfo{
Media: media,
Website: "http://example.com/some-snap",
StoreURL: "https://snapcraft.io/some-snap",
}
c.Assert(snapstate.KeepAuxStoreInfo(info.SnapID, aux), check.IsNil)
c.Assert(backend.KeepAuxStoreInfo(info.SnapID, aux), check.IsNil)
c.Check(osutil.FileExists(filename), check.Equals, true)

c.Assert(snapstate.RetrieveAuxStoreInfo(info), check.IsNil)
c.Assert(backend.RetrieveAuxStoreInfo(info), check.IsNil)
c.Check(info.Media, check.HasLen, 1)
c.Check(info.Media, check.DeepEquals, media)
c.Check(info.Website(), check.Equals, "http://example.com/some-snap")
Expand All @@ -75,7 +75,7 @@ func (s *auxInfoSuite) TestAuxStoreInfoRoundTrip(c *check.C) {
info.EditedLinks = map[string][]string{
"website": {"http://newer-website-com"},
}
c.Assert(snapstate.RetrieveAuxStoreInfo(info), check.IsNil)
c.Assert(backend.RetrieveAuxStoreInfo(info), check.IsNil)
c.Check(info.Media, check.HasLen, 1)
c.Check(info.Media, check.DeepEquals, media)
c.Check(info.Website(), check.Equals, "http://newer-website-com")
Expand All @@ -86,13 +86,13 @@ func (s *auxInfoSuite) TestAuxStoreInfoRoundTrip(c *check.C) {
info.LegacyWebsite = ""
info.StoreURL = ""

c.Assert(snapstate.DiscardAuxStoreInfo(info.SnapID), check.IsNil)
c.Assert(backend.DiscardAuxStoreInfo(info.SnapID), check.IsNil)
c.Assert(osutil.FileExists(filename), check.Equals, false)

c.Check(snapstate.RetrieveAuxStoreInfo(info), check.IsNil)
c.Check(backend.RetrieveAuxStoreInfo(info), check.IsNil)
c.Check(info.Media, check.HasLen, 0)
c.Check(info.Website(), check.Equals, "")
c.Check(info.StoreURL, check.Equals, "")

c.Check(snapstate.DiscardAuxStoreInfo(info.SnapID), check.IsNil)
c.Check(backend.DiscardAuxStoreInfo(info.SnapID), check.IsNil)
}
3 changes: 3 additions & 0 deletions overlord/snapstate/backend/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ var (
RemoveIfEmpty = removeIfEmpty
SnapDataDirs = snapDataDirs
SnapCommonDataDirs = snapCommonDataDirs

KeepAuxStoreInfo = keepAuxStoreInfo
DiscardAuxStoreInfo = discardAuxStoreInfo
)

func MockWrappersAddSnapdSnapServices(f func(s *snap.Info, opts *wrappers.AddSnapdSnapServicesOptions, inter wrappers.Interacter) (wrappers.SnapdRestart, error)) (restore func()) {
Expand Down
48 changes: 48 additions & 0 deletions overlord/snapstate/backend/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2025 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package backend

// InstallStoreMetadata saves revision-agnostic metadata to disk for the snap
// with the given snap ID. At the moment, this metadata includes auxiliary
// store information.
func InstallStoreMetadata(snapID string, aux *AuxStoreInfo) error {
if snapID == "" {
return nil
}
if err := keepAuxStoreInfo(snapID, aux); err != nil {
return err
}
// TODO: install other types of revision-agnostic metadata
return nil
}

// DiscardStoreMetadata removes revision-agnostic metadata to disk for the snap
// with the given snap ID. At the moment, this metadata includes auxiliary
// store information. If hasOtherInstances is true, does nothing.
func DiscardStoreMetadata(snapID string, hasOtherInstances bool) error {
if hasOtherInstances || snapID == "" {
return nil
}
if err := discardAuxStoreInfo(snapID); err != nil {
return err
}
// TODO: discard other types of revision-agnostic metadata
return nil
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if these could be called directly from within other backend methods (e.g. backend.LinkSnap and backend.UnlinkSnap), as that would mean nearly/all of the complexity around saving and reverting auxinfo could be avoided. However, that would require those methods to be able to compute hasOtherInstances, which is not possible at the moment, unless we want to start passing snapstate into the backend.

Notably, the error handling for doLinkSnap after backend.LinkSnap(newInfo, ...) has succeeded already does exactly what we want: if there's another revision, simply call backend.LinkSnap(oldInfo, ...), and if there's no other revision, call backend.UnlinkSnap. If InstallStoreMetadata and DiscardStoreMetadata were called from within these, respectively, the result would be as desired.

There are lots of other backend functions which are called from the handlers though, so I'm not sure if it would be consistent to place calls to these helpers within backend.LinkSnap and backend.UnlinkSnap, for example, else why are so many backend helpers called from within

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to hear your thoughts on this next week @bboozzoo and @pedronis

100 changes: 100 additions & 0 deletions overlord/snapstate/backend/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2025 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package backend_test

import (
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
)

type metadataSuite struct{}

var _ = Suite(&metadataSuite{})

func (s *metadataSuite) SetUpTest(c *C) {
dirs.SetRootDir(c.MkDir())
}

func (s *metadataSuite) TestStoreMetadataRoundTrip(c *C) {
const snapID = "my-snap-id"
const hasOtherInstances = false
c.Assert(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)
aux := &backend.AuxStoreInfo{
Media: snap.MediaInfos{
snap.MediaInfo{
Type: "icon",
URL: "http://images.com/my-icon",
Width: 128,
Height: 128,
},
snap.MediaInfo{
Type: "website",
URL: "http://another.com",
},
},
StoreURL: "https://snapcraft.io/example-snap",
Website: "http://example.com",
}
c.Check(backend.InstallStoreMetadata(snapID, aux), IsNil)
c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FilePresent)

var info snap.Info
info.SnapID = snapID
c.Check(backend.RetrieveAuxStoreInfo(&info), IsNil)
c.Check(info.Media, DeepEquals, aux.Media)
c.Check(info.LegacyWebsite, Equals, aux.Website)
c.Check(info.StoreURL, Equals, aux.StoreURL)

c.Check(backend.DiscardStoreMetadata(snapID, hasOtherInstances), IsNil)
c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)
}

func (s *metadataSuite) TestStoreMetadataEmptySnapID(c *C) {
const snapID = ""
const hasOtherInstances = false
var aux *backend.AuxStoreInfo
// check that empty snapID does not return an error
c.Check(backend.InstallStoreMetadata(snapID, aux), IsNil)
c.Check(backend.DiscardStoreMetadata(snapID, hasOtherInstances), IsNil)
}

func (s *metadataSuite) TestDiscardStoreMetadataHasOtherInstances(c *C) {
const snapID = "my-snap-id"
c.Assert(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)
aux := &backend.AuxStoreInfo{
StoreURL: "https://snapcraft.io/example-snap",
}
c.Check(backend.InstallStoreMetadata(snapID, aux), IsNil)
c.Check(backend.AuxStoreInfoFilename(snapID), testutil.FilePresent)

// Check that it does not discard if hasOtherInstances is true
hasOtherInstances := true
c.Check(backend.DiscardStoreMetadata(snapID, hasOtherInstances), IsNil)
c.Assert(backend.AuxStoreInfoFilename(snapID), testutil.FilePresent)

hasOtherInstances = false
// Check that it is discarded if hasOtherInstances is false
c.Check(backend.DiscardStoreMetadata(snapID, hasOtherInstances), IsNil)
c.Assert(backend.AuxStoreInfoFilename(snapID), testutil.FileAbsent)
}
9 changes: 0 additions & 9 deletions overlord/snapstate/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,6 @@ func MockReRefreshRetryTimeout(d time.Duration) (restore func()) {
}
}

// aux store info
var (
AuxStoreInfoFilename = auxStoreInfoFilename
RetrieveAuxStoreInfo = retrieveAuxStoreInfo
KeepAuxStoreInfo = keepAuxStoreInfo
DiscardAuxStoreInfo = discardAuxStoreInfo
)

type AuxStoreInfo = auxStoreInfo
type DisabledServices = disabledServices

// link, misc handlers
Expand Down
Loading
Loading