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: send components information on GET /v2/snaps #14060

Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions client/clientutil/snapinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func (*cmdSuite) TestClientSnapFromSnapInfo(c *C) {
"Hold",
"GatingHold",
"RefreshInhibit",
"Components",
}
var checker func(string, reflect.Value)
checker = func(pfx string, x reflect.Value) {
Expand Down
40 changes: 40 additions & 0 deletions client/components.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 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 client

import (
"time"

"github.com/snapcore/snapd/snap"
)

// Component describes a component for API purposes
// TODO we will eventually add a "status" field when it becomes clear which
// states can a component have.
type Component struct {
Name string `json:"name"`
Type snap.ComponentType `json:"type"`
Version string `json:"version"`
Summary string `json:"summary"`
Description string `json:"description"`
Revision snap.Revision `json:"revision"`
InstalledSize int64 `json:"installed-size,omitempty"`
InstallDate *time.Time `json:"install-date,omitempty"`
}
3 changes: 3 additions & 0 deletions client/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ type Snap struct {
GatingHold *time.Time `json:"gating-hold,omitempty"`
// if RefreshInhibit is nil, then there is no pending refresh.
RefreshInhibit *SnapRefreshInhibit `json:"refresh-inhibit,omitempty"`

// Components is a list of the snap components
Components []Component `json:"components,omitempty"`
}

type SnapHealth struct {
Expand Down
5 changes: 4 additions & 1 deletion daemon/api_sideload_n_try.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,18 +325,21 @@ func sideloadSnap(st *state.State, snapFile *uploadedSnap, flags sideloadFlags)

var tset *state.TaskSet
container := "snap"
message := fmt.Sprintf("%q snap", instanceName)
if compInfo == nil {
tset, _, err = snapstateInstallPath(st, sideInfo, snapFile.tmpPath, instanceName, "", flags.Flags, nil)
} else {
// It is a component
container = "component"
message = fmt.Sprintf("%q component for %q snap",
compInfo.Component.ComponentName, instanceName)
tset, err = snapstateInstallComponentPath(st, snap.NewComponentSideInfo(compInfo.Component, snap.Revision{}), snapInfo, snapFile.tmpPath, flags.Flags)
}
if err != nil {
return nil, errToResponse(err, []string{sideInfo.RealName}, InternalError, "cannot install %s file: %v", container)
}

msg := fmt.Sprintf(i18n.G("Install %q %s from file %q"), instanceName, container, snapFile.filename)
msg := fmt.Sprintf(i18n.G("Install %s from file %q"), message, snapFile.filename)
chg := newChange(st, "install-"+container, msg, []*state.TaskSet{tset}, []string{instanceName})
apiData := map[string]interface{}{
"snap-name": instanceName,
Expand Down
4 changes: 2 additions & 2 deletions daemon/api_sideload_n_try_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func (s *sideloadSuite) TestSideloadComponent(c *check.C) {
csi := snap.NewComponentSideInfo(naming.NewComponentRef("local", "comp"), snap.Revision{})

chgSummary, systemRestartImmediate := s.sideloadComponentCheck(c, body, head, "local", flags, csi)
c.Check(chgSummary, check.Equals, `Install "local" component from file "a/b/local+localcomp.comp"`)
c.Check(chgSummary, check.Equals, `Install "comp" component for "local" snap from file "a/b/local+localcomp.comp"`)
c.Check(systemRestartImmediate, check.Equals, false)
}

Expand All @@ -311,7 +311,7 @@ func (s *sideloadSuite) TestSideloadComponentInstanceName(c *check.C) {
csi := snap.NewComponentSideInfo(naming.NewComponentRef("local", "comp"), snap.Revision{})

chgSummary, systemRestartImmediate := s.sideloadComponentCheck(c, body, head, "local_instance", flags, csi)
c.Check(chgSummary, check.Equals, `Install "local_instance" component from file "a/b/local+localcomp.comp"`)
c.Check(chgSummary, check.Equals, `Install "comp" component for "local_instance" snap from file "a/b/local+localcomp.comp"`)
c.Check(systemRestartImmediate, check.Equals, false)
}

Expand Down
204 changes: 204 additions & 0 deletions daemon/api_snaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ import (
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/healthstate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/snapstate/sequence"
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/sandbox"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/strutil"
Expand Down Expand Up @@ -1605,6 +1608,207 @@ func (s *snapsSuite) TestMapLocalFields(c *check.C) {
c.Check(daemon.MapLocal(about, nil), check.DeepEquals, expected)
}

func (s *snapsSuite) TestMapLocalFieldsWithComponents(c *check.C) {
dirs.SetRootDir(c.MkDir())
defer dirs.SetRootDir(dirs.GlobalRootDir)

media := snap.MediaInfos{
{
Type: "screenshot",
URL: "https://example.com/shot1.svg",
}, {
Type: "icon",
URL: "https://example.com/icon.png",
}, {
Type: "screenshot",
URL: "https://example.com/shot2.svg",
},
}

publisher := snap.StoreAccount{
ID: "some-dev-id",
Username: "some-dev",
DisplayName: "Some Developer",
Validation: "poor",
}
info := &snap.Info{
SideInfo: snap.SideInfo{
SnapID: "some-snap-id",
RealName: "some-snap",
EditedTitle: "A Title",
EditedSummary: "a summary",
EditedDescription: "the\nlong\ndescription",
Channel: "bleeding/edge",
EditedLinks: map[string][]string{
"contact": {"mailto:[email protected]"},
},
LegacyEditedContact: "mailto:[email protected]",
Revision: snap.R(7),
Private: true,
},
SnapType: "app",
Base: "the-base",
Version: "v1.0",
License: "MIT",
Broken: "very",
Confinement: "very strict",
CommonIDs: []string{"foo", "bar"},
Media: media,
DownloadInfo: snap.DownloadInfo{
Size: 42,
Sha3_384: "some-sum",
},
Publisher: publisher,
Components: map[string]*snap.Component{
"comp-1": {
Name: "comp-1",
Type: "test",
},
"comp-2": {
Name: "comp-2",
Type: "test",
Summary: "summary 2",
Description: "description 2",
},
"comp-3": {
Name: "comp-3",
Type: "test",
Summary: "summary 3",
Description: "description 3",
},
"comp-4": {
Name: "comp-4",
Type: "test",
},
},
}

// make InstallDate work
c.Assert(os.MkdirAll(info.MountDir(), 0755), check.IsNil)
c.Assert(os.Symlink("7", filepath.Join(info.MountDir(), "..", "current")), check.IsNil)

info.Apps = map[string]*snap.AppInfo{
"foo": {Snap: info, Name: "foo", Command: "foo"},
"bar": {Snap: info, Name: "bar", Command: "bar"},
}

const comp1yaml = `
component: some-snap+comp-1
type: test
version: 1.0
`
const comp2yaml = `
component: some-snap+comp-2
type: test
version: 1.0
summary: summary 2
description: description 2
`
// We need just enough info for components in snap.yaml
const snapYaml = `
name: some-snap
version: 1
components:
comp-1:
type: test
comp-2:
type: test
`

// Mock snap.yaml/component.yaml files for installed components
ssi := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(7),
SnapID: "some-snap-id"}
snaptest.MockSnap(c, snapYaml, ssi)
csi := snap.NewComponentSideInfo(naming.NewComponentRef("some-snap", "comp-1"), snap.R(33))
snaptest.MockComponent(c, comp1yaml, info, *csi)
csi2 := snap.NewComponentSideInfo(naming.NewComponentRef("some-snap", "comp-2"), snap.R(34))
snaptest.MockComponent(c, comp2yaml, info, *csi2)
comps := []*sequence.ComponentState{
sequence.NewComponentState(csi, snap.TestComponent),
sequence.NewComponentState(csi2, snap.TestComponent),
}

// make InstallDate/InstalledSize work for comp1 and comp2
cpi := snap.MinimalComponentContainerPlaceInfo(
csi.Component.ComponentName, csi.Revision, "some-snap")
symLn := snap.ComponentLinkPath(cpi, snap.R(7))
c.Assert(os.MkdirAll(cpi.MountDir(), 0755), check.IsNil)
os.WriteFile(cpi.MountFile(), []byte{0, 0}, 0644)
c.Assert(os.MkdirAll(filepath.Dir(symLn), 0755), check.IsNil)
c.Assert(os.Symlink(cpi.MountDir(), symLn), check.IsNil)
cpi2 := snap.MinimalComponentContainerPlaceInfo(
csi2.Component.ComponentName, csi2.Revision, "some-snap")
symLn2 := snap.ComponentLinkPath(cpi2, snap.R(7))
c.Assert(os.MkdirAll(cpi2.MountDir(), 0755), check.IsNil)
os.WriteFile(cpi2.MountFile(), []byte{0, 0, 0}, 0644)
c.Assert(os.MkdirAll(filepath.Dir(symLn2), 0755), check.IsNil)
c.Assert(os.Symlink(cpi2.MountDir(), symLn2), check.IsNil)

about := daemon.MakeAboutSnap(info, &snapstate.SnapState{
Sequence: snapstatetest.NewSequenceFromRevisionSideInfos(
[]*sequence.RevisionSideState{
sequence.NewRevisionSideState(ssi, comps)}),
Active: true,
TrackingChannel: "flaky/beta",
Current: snap.R(7),
Flags: snapstate.Flags{
IgnoreValidation: true,
DevMode: true,
JailMode: true,
},
},
)

expected := &client.Snap{
ID: "some-snap-id",
Name: "some-snap",
Summary: "a summary",
Description: "the\nlong\ndescription",
Developer: "some-dev",
Publisher: &publisher,
Icon: "https://example.com/icon.png",
Type: "app",
Base: "the-base",
Version: "v1.0",
Revision: snap.R(7),
Channel: "bleeding/edge",
TrackingChannel: "flaky/beta",
InstallDate: info.InstallDate(),
InstalledSize: 42,
Status: "active",
Confinement: "very strict",
IgnoreValidation: true,
DevMode: true,
JailMode: true,
Private: true,
Broken: "very",
Links: map[string][]string{
"contact": {"mailto:[email protected]"},
},
Contact: "mailto:[email protected]",
Title: "A Title",
License: "MIT",
CommonIDs: []string{"foo", "bar"},
MountedFrom: filepath.Join(dirs.SnapBlobDir, "some-snap_7.snap"),
Media: media,
Apps: []client.AppInfo{
{Snap: "some-snap", Name: "bar"},
{Snap: "some-snap", Name: "foo"},
},
Components: []client.Component{
{Name: "comp-1", Type: "test", Version: "1.0", Revision: snap.R(33),
InstallDate: snap.ComponentInstallDate(cpi, snap.R(7)), InstalledSize: 2},
{Name: "comp-2", Type: "test", Version: "1.0", Revision: snap.R(34),
Summary: "summary 2", Description: "description 2",
InstallDate: snap.ComponentInstallDate(cpi, snap.R(7)), InstalledSize: 3},
{Name: "comp-3", Type: "test",
Summary: "summary 3", Description: "description 3"},
{Name: "comp-4", Type: "test"},
},
}
c.Check(daemon.MapLocal(about, nil), check.DeepEquals, expected)
}

func (s *snapsSuite) TestMapLocalOfTryResolvesSymlink(c *check.C) {
c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil)

Expand Down
53 changes: 53 additions & 0 deletions daemon/snap.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,62 @@ func mapLocal(about aboutSnap, sd clientutil.StatusDecorator) *client.Snap {
if !about.gatingHold.IsZero() {
result.GatingHold = &about.gatingHold
}

if len(about.info.Components) > 0 {
result.Components = fillComponentInfo(about)
}

return result
}

func fillComponentInfo(about aboutSnap) []client.Component {
localSnap, snapst := about.info, about.snapst
comps := make([]client.Component, 0, len(about.info.Components))

// First present installed components
currentComps, err := snapst.CurrentComponentInfos()
if err != nil {
logger.Noticef("cannot retrieve installed components: %v", err)
}
currentCompsSet := map[string]bool{}
for _, comp := range currentComps {
currentCompsSet[comp.Component.ComponentName] = true
csi := snapst.CurrentComponentSideInfo(comp.Component)
cpi := snap.MinimalComponentContainerPlaceInfo(
comp.Component.ComponentName, csi.Revision, localSnap.InstanceName())
compSz, err := snap.ComponentSize(cpi)
if err != nil {
logger.Noticef("cannot get size of %s: %v", comp.Component, err)
compSz = 0
}
comps = append(comps, client.Component{
Name: comp.Component.ComponentName,
Type: comp.Type,
Version: comp.Version,
Summary: comp.Summary,
Description: comp.Description,
Revision: csi.Revision,
InstallDate: snap.ComponentInstallDate(cpi, localSnap.Revision),
InstalledSize: compSz,
})
}

// Then, non-installed components
for name, comp := range about.info.Components {
if _, ok := currentCompsSet[name]; ok {
continue
}
comps = append(comps, client.Component{
Name: name,
Type: comp.Type,
Summary: comp.Summary,
Description: comp.Description,
})
}

return comps
}

// snapIcon tries to find the icon inside the snap
func snapIcon(info snap.PlaceInfo) string {
found, _ := filepath.Glob(filepath.Join(info.MountDir(), "meta", "gui", "icon.*"))
Expand Down
Loading
Loading