Skip to content
Draft
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
17 changes: 13 additions & 4 deletions cmd/chisel/cmd_cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ to create a new filesystem tree in the root location.

By default it fetches the slices for the same Ubuntu version as the
current host, unless the --release flag is used.

Slices are named <package>_<slice>. For packages coming from a store, a
channel can be appended to select which one to fetch, as in
mybin_myslice@2.0/edge. A channel is a <track>/<risk> value.

The risk defaults to stable, so @2.0 and @2.0/stable are equivalent. When
no channel is given at all, the default track of the package is used,
again with the stable risk. Slices of the same package must all agree on
the channel.
`

var cutDescs = map[string]string{
Expand Down Expand Up @@ -49,13 +58,13 @@ func (cmd *cmdCut) Execute(args []string) error {
return ErrExtraArgs
}

sliceKeys := make([]setup.SliceKey, len(cmd.Positional.SliceRefs))
sliceRefs := make([]setup.SliceRef, len(cmd.Positional.SliceRefs))
for i, sliceRef := range cmd.Positional.SliceRefs {
sliceKey, err := setup.ParseSliceKey(sliceRef)
ref, err := setup.ParseSliceRef(sliceRef)
if err != nil {
return err
}
sliceKeys[i] = sliceKey
sliceRefs[i] = ref
}

release, err := obtainRelease(cmd.Release)
Expand All @@ -73,7 +82,7 @@ func (cmd *cmdCut) Execute(args []string) error {
}
}

selection, err := setup.Select(release, sliceKeys, cmd.Arch)
selection, err := setup.Select(release, sliceRefs, cmd.Arch)
if err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/chisel/cmd_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ func match(slice *setup.Slice, query string) bool {
// findSlices returns slices from the provided release that match all of the
// query strings (AND).
func findSlices(release *setup.Release, query []string) (slices []*setup.Slice, err error) {
for _, term := range query {
if strings.Contains(term, "@") {
return nil, fmt.Errorf("invalid slice reference %q: slices are not specific to a channel", term)
}
}
slices = []*setup.Slice{}
for _, pkg := range release.Packages {
for _, slice := range pkg.Slices {
Expand Down
10 changes: 10 additions & 0 deletions cmd/chisel/cmd_find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type findTest struct {
release *setup.Release
query []string
result []*setup.Slice
err string
}

func makeSamplePackage(pkg string, slices []string) *setup.Package {
Expand Down Expand Up @@ -123,6 +124,11 @@ var findTests = []findTest{{
release: sampleRelease,
query: []string{"python", "slice"},
result: []*setup.Slice{},
}, {
summary: "Slices are not specific to a channel",
release: sampleRelease,
query: []string{"python3.10_bins@3.0"},
err: `invalid slice reference "python3.10_bins@3.0": slices are not specific to a channel`,
}}

func (s *ChiselSuite) TestFindSlices(c *C) {
Expand All @@ -131,6 +137,10 @@ func (s *ChiselSuite) TestFindSlices(c *C) {

for _, query := range testutil.Permutations(test.query) {
slices, err := chisel.FindSlices(test.release, query)
if test.err != "" {
c.Assert(err, ErrorMatches, test.err)
continue
}
c.Assert(err, IsNil)
c.Assert(slices, DeepEquals, test.result)
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/chisel/cmd_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ func (cmd *infoCmd) Execute(args []string) error {
return err
}

for _, query := range cmd.Positional.Queries {
if strings.Contains(query, "@") {
return fmt.Errorf("invalid slice reference %q: slices are not specific to a channel", query)
}
}

packages, notFound := selectPackageSlices(release, cmd.Positional.Queries)

for i, pkg := range packages {
Expand Down
5 changes: 5 additions & 0 deletions cmd/chisel/cmd_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ var infoTests = []infoTest{{
input: infoRelease,
query: []string{"foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"},
err: `no slice definitions found for: "foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"`,
}, {
summary: "Slices are not specific to a channel",
input: infoRelease,
query: []string{"mypkg1_myslice1@3.0"},
err: `invalid slice reference "mypkg1_myslice1@3.0": slices are not specific to a channel`,
}}

var infoRelease = map[string]string{
Expand Down
56 changes: 56 additions & 0 deletions internal/setup/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,62 @@ func (c Channel) String() string {
return channel
}

var channelRisks = []string{"stable", "candidate", "beta", "edge"}

// parseChannel parses a "<track>[/<risk>[/<branch>]]" string, representing
// a store channel.
func parseChannel(s string) (Channel, error) {
if s == "" {
return Channel{}, errors.New("missing channel")
}
if strings.ContainsFunc(s, unicode.IsSpace) {
return Channel{}, errors.New("channel must not contain spaces")
}
p := strings.Split(s, "/")
var risk, track, branch *string
switch len(p) {
default:
return Channel{}, fmt.Errorf("channel must be <track>[/<risk>[/<branch>]]: %s", s)
case 3:
track, risk, branch = &p[0], &p[1], &p[2]
case 2:
if slices.Contains(channelRisks, p[0]) {
risk, branch = &p[0], &p[1]
} else {
track, risk = &p[0], &p[1]
}
case 1:
if slices.Contains(channelRisks, p[0]) {
risk = &p[0]
} else {
track = &p[0]
}
}

ch := Channel{}

if risk != nil {
if !slices.Contains(channelRisks, *risk) {
return Channel{}, fmt.Errorf("invalid risk in channel name: %s", s)
}
ch.Risk = *risk
}
if track != nil {
if *track == "" {
return Channel{}, fmt.Errorf("invalid track in channel name: %s", s)
}
ch.Track = *track
}
if branch != nil {
if *branch == "" {
return Channel{}, fmt.Errorf("invalid branch in channel name: %s", s)
}
ch.Branch = *branch
}

return ch, nil
}

// The form a channel pattern must take, as reported to the user.
const channelPatternForm = "<track>/<risk>"

Expand Down
47 changes: 41 additions & 6 deletions internal/setup/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ import (
"github.com/canonical/chisel/internal/setup"
)

var channelStringTests = []struct {
summary string
channel setup.Channel
expected string
}{{
summary: "An unset channel renders as empty",
channel: setup.Channel{},
expected: "",
}, {
summary: "A track and a risk",
channel: setup.Channel{Track: "3.0", Risk: "stable"},
expected: "3.0/stable",
}, {
summary: "A branch is appended",
channel: setup.Channel{Track: "3.0", Risk: "edge", Branch: "mybranch"},
expected: "3.0/edge/mybranch",
}, {
// A channel is never built without a risk, but rendering the risk as
// optional would turn the branch into one, that is a different channel.
summary: "A missing risk is not skipped over",
channel: setup.Channel{Track: "3.0", Branch: "mybranch"},
expected: "3.0//mybranch",
}, {
summary: "A missing track is visible",
channel: setup.Channel{Risk: "edge"},
expected: "/edge",
}}

func (s *S) TestChannelString(c *C) {
for _, test := range channelStringTests {
c.Logf("Summary: %s", test.summary)
c.Assert(test.channel.String(), Equals, test.expected)
}
}

// channelPatternTests covers validating and matching the patterns of a
// "channel" field. The valid patterns come first, then the invalid ones,
// grouped after the validation phase they exercise. Note several of the latter
Expand Down Expand Up @@ -132,8 +167,8 @@ var channelPatternTests = []struct {
}, {
// The "!<risk>" form.
summary: "Unknown excluded risk",
values: []string{"0.3/!whatever"},
err: `"0.3/!whatever": unknown risk "whatever", must be one of stable, candidate, beta, edge`,
values: []string{"0.3/!invalid"},
err: `"0.3/!invalid": unknown risk "invalid", must be one of stable, candidate, beta, edge`,
}, {
summary: "Exclusion combined with other risks",
values: []string{"0.3/!stable,edge"},
Expand All @@ -145,12 +180,12 @@ var channelPatternTests = []struct {
}, {
// The "<risk>[,<risk>]" form.
summary: "Unknown risk",
values: []string{"0.3/whatever"},
err: `"0.3/whatever": unknown risk "whatever", must be one of stable, candidate, beta, edge`,
values: []string{"0.3/invalid"},
err: `"0.3/invalid": unknown risk "invalid", must be one of stable, candidate, beta, edge`,
}, {
summary: "Unknown risk in a list",
values: []string{"0.3/edge,whatever"},
err: `"0.3/edge,whatever": unknown risk "whatever", must be one of stable, candidate, beta, edge`,
values: []string{"0.3/edge,invalid"},
err: `"0.3/edge,invalid": unknown risk "invalid", must be one of stable, candidate, beta, edge`,
}, {
summary: "Risks are case sensitive",
values: []string{"0.3/Stable"},
Expand Down
79 changes: 73 additions & 6 deletions internal/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,41 @@ func ParseSliceKey(sliceKey string) (SliceKey, error) {
return apacheutil.ParseSliceKey(sliceKey)
}

// DefaultRisk is used when a channel is resolved from a track alone, as done
// for the 'default-track' of a store package.
// DefaultRisk is used when a slice reference does not specify a risk.
const DefaultRisk = "stable"

// SliceRef is a slice reference with an optional channel for store packages.
// The channel always holds a risk, the default one is used when the reference
// does not specify it.
type SliceRef struct {
SliceKey SliceKey
Channel Channel
}

// ParseSliceRef parses a "pkg_slice[@channel]" reference.
func ParseSliceRef(ref string) (SliceRef, error) {
keyPart, channel, ok := strings.Cut(ref, "@")
if !ok {
sliceKey, err := ParseSliceKey(ref)
if err != nil {
return SliceRef{}, err
}
return SliceRef{SliceKey: sliceKey}, nil
}
sliceKey, err := ParseSliceKey(keyPart)
if err != nil {
return SliceRef{}, err
}
parsed, err := parseChannel(channel)
if err != nil {
return SliceRef{}, fmt.Errorf("invalid slice reference %q: %s", ref, err)
}
if parsed.Risk == "" {
parsed.Risk = DefaultRisk
}
return SliceRef{SliceKey: sliceKey, Channel: parsed}, nil
}

func (s *Slice) String() string { return s.Package + "_" + s.Name }

// Selection holds the required configuration to create a Build for a selection
Expand Down Expand Up @@ -507,7 +538,7 @@ func stripBase(baseDir, path string) string {
return strings.TrimPrefix(path, baseDir+string(filepath.Separator))
}

func Select(release *Release, slices []SliceKey, arch string) (*Selection, error) {
func Select(release *Release, refs []SliceRef, arch string) (*Selection, error) {
logf("Selecting slices...")

var err error
Expand All @@ -523,12 +554,19 @@ func Select(release *Release, slices []SliceKey, arch string) (*Selection, error
// Select the channel of every store package, whether it is selected or
// not, and before ordering, because ordering depends on the channel of the
// packages it traverses.
channels := selectChannels(release)
channels, err := selectChannels(release, refs)
if err != nil {
return nil, err
}

selection := &Selection{
Release: release,
}

slices := make([]SliceKey, len(refs))
for i, ref := range refs {
slices[i] = ref.SliceKey
}
sorted, err := order(release.Packages, slices, arch, channels)
if err != nil {
return nil, err
Expand Down Expand Up @@ -580,15 +618,44 @@ func Select(release *Release, slices []SliceKey, arch string) (*Selection, error
// selectChannels returns the channel of every store package of the release,
// derived from its 'default-track' with the default risk. Note the release
// only defines a track, the risk is implicit.
func selectChannels(release *Release) map[string]Channel {
//
// It errors if a channel is set on a non-store package or if two references to
// the same package specify different channels.
func selectChannels(release *Release, refs []SliceRef) (map[string]Channel, error) {
channels := make(map[string]Channel)
for _, ref := range refs {
pkg, ok := release.Packages[ref.SliceKey.Package]
if !ok {
// Nothing to validate; the package is unknown.
continue
}
if pkg.Store == "" {
if ref.Channel != (Channel{}) {
return nil, fmt.Errorf("slice %s has channel but package %q is not in a store",
ref.SliceKey, pkg.Name)
}
continue
}
if ref.Channel == (Channel{}) {
continue
}
if existing, ok := channels[pkg.Name]; ok && existing != ref.Channel {
return nil, fmt.Errorf("slices of package %q have conflicting channels %q and %q",
pkg.Name, existing, ref.Channel)
}
channels[pkg.Name] = ref.Channel
}
for _, pkg := range release.Packages {
if pkg.Store == "" {
continue
}
if _, ok := channels[pkg.Name]; ok {
// The references take precedence over the 'default-track'.
continue
}
channels[pkg.Name] = Channel{Track: pkg.DefaultTrack, Risk: DefaultRisk}
}
return channels
return channels, nil
}

const (
Expand Down
Loading
Loading