/
channel.go
297 lines (266 loc) · 7.6 KB
/
channel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018-2019 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 channel
import (
"errors"
"fmt"
"strings"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/strutil"
)
var channelRisks = []string{"stable", "candidate", "beta", "edge"}
// Channel identifies and describes completely a store channel.
type Channel struct {
Architecture string `json:"architecture"`
Name string `json:"name"`
Track string `json:"track"`
Risk string `json:"risk"`
Branch string `json:"branch,omitempty"`
}
func isSlash(r rune) bool { return r == '/' }
// TODO: currently there's some overlap between the toplevel Full, and
// methods Clean, String, and Full. Needs further refactoring.
func Full(s string) (string, error) {
if s == "" {
return "", nil
}
components := strings.FieldsFunc(s, isSlash)
switch len(components) {
case 0:
return "", nil
case 1:
if strutil.ListContains(channelRisks, components[0]) {
return "latest/" + components[0], nil
}
return components[0] + "/stable", nil
case 2:
if strutil.ListContains(channelRisks, components[0]) {
return "latest/" + strings.Join(components, "/"), nil
}
fallthrough
case 3:
return strings.Join(components, "/"), nil
default:
return "", errors.New("invalid channel")
}
}
// ParseVerbatim parses a string representing a store channel and
// includes the given architecture, if architecture is "" the system
// architecture is included. The channel representation is not normalized.
// Parse() should be used in most cases.
func ParseVerbatim(s string, architecture string) (Channel, error) {
if s == "" {
return Channel{}, fmt.Errorf("channel name cannot be empty")
}
p := strings.Split(s, "/")
var risk, track, branch *string
switch len(p) {
default:
return Channel{}, fmt.Errorf("channel name has too many components: %s", s)
case 3:
track, risk, branch = &p[0], &p[1], &p[2]
case 2:
if strutil.ListContains(channelRisks, p[0]) {
risk, branch = &p[0], &p[1]
} else {
track, risk = &p[0], &p[1]
}
case 1:
if strutil.ListContains(channelRisks, p[0]) {
risk = &p[0]
} else {
track = &p[0]
}
}
if architecture == "" {
architecture = arch.DpkgArchitecture()
}
ch := Channel{
Architecture: architecture,
}
if risk != nil {
if !strutil.ListContains(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
}
// Parse parses a string representing a store channel and includes given
// architecture, , if architecture is "" the system architecture is included.
// The returned channel's track, risk and name are normalized.
func Parse(s string, architecture string) (Channel, error) {
channel, err := ParseVerbatim(s, architecture)
if err != nil {
return Channel{}, err
}
return channel.Clean(), nil
}
// Clean returns a Channel with a normalized track, risk and name.
func (c Channel) Clean() Channel {
track := c.Track
risk := c.Risk
if track == "latest" {
track = ""
}
if risk == "" {
risk = "stable"
}
// normalized name
name := risk
if track != "" {
name = track + "/" + name
}
if c.Branch != "" {
name = name + "/" + c.Branch
}
return Channel{
Architecture: c.Architecture,
Name: name,
Track: track,
Risk: risk,
Branch: c.Branch,
}
}
func (c Channel) String() string {
return c.Name
}
// Full returns the full name of the channel, inclusive the default track "latest".
func (c *Channel) Full() string {
ch := c.String()
full, err := Full(ch)
if err != nil {
// unpossible
panic("channel.String() returned a malformed channel: " + ch)
}
return full
}
// VerbatimTrackOnly returns whether the channel represents a track only.
func (c *Channel) VerbatimTrackOnly() bool {
return c.Track != "" && c.Risk == "" && c.Branch == ""
}
// VerbatimRiskOnly returns whether the channel represents a risk only.
func (c *Channel) VerbatimRiskOnly() bool {
return c.Track == "" && c.Risk != "" && c.Branch == ""
}
func riskLevel(risk string) int {
for i, r := range channelRisks {
if r == risk {
return i
}
}
return -1
}
// ChannelMatch represents on which fields two channels are matching.
type ChannelMatch struct {
Architecture bool
Track bool
Risk bool
}
// String returns the string represantion of the match, results can be:
// "architecture:track:risk"
// "architecture:track"
// "architecture:risk"
// "track:risk"
// "architecture"
// "track"
// "risk"
// ""
func (cm ChannelMatch) String() string {
matching := []string{}
if cm.Architecture {
matching = append(matching, "architecture")
}
if cm.Track {
matching = append(matching, "track")
}
if cm.Risk {
matching = append(matching, "risk")
}
return strings.Join(matching, ":")
}
// Match returns a ChannelMatch of which fields among architecture,track,risk match between c and c1 store channels, risk is matched taking channel inheritance into account and considering c the requested channel.
func (c *Channel) Match(c1 *Channel) ChannelMatch {
requestedRiskLevel := riskLevel(c.Risk)
rl1 := riskLevel(c1.Risk)
return ChannelMatch{
Architecture: c.Architecture == c1.Architecture,
Track: c.Track == c1.Track,
Risk: requestedRiskLevel >= rl1,
}
}
// Resolve resolves newChannel wrt channel, this means if newChannel
// is risk/branch only it will preserve the track of channel. It
// assumes that if both are not empty, channel is parseable.
func Resolve(channel, newChannel string) (string, error) {
if newChannel == "" {
return channel, nil
}
if channel == "" {
return newChannel, nil
}
ch, err := ParseVerbatim(channel, "-")
if err != nil {
return "", err
}
p := strings.Split(newChannel, "/")
if strutil.ListContains(channelRisks, p[0]) && ch.Track != "" {
// risk/branch inherits the track if any
return ch.Track + "/" + newChannel, nil
}
return newChannel, nil
}
var ErrPinnedTrackSwitch = errors.New("cannot switch pinned track")
// ResolvePinned resolves newChannel wrt a pinned track, newChannel
// can only be risk/branch-only or have the same track, otherwise
// ErrPinnedTrackSwitch is returned.
func ResolvePinned(track, newChannel string) (string, error) {
if track == "" {
return newChannel, nil
}
ch, err := ParseVerbatim(track, "-")
if err != nil || !ch.VerbatimTrackOnly() {
return "", fmt.Errorf("invalid pinned track: %s", track)
}
if newChannel == "" {
return track, nil
}
trackPrefix := ch.Track + "/"
p := strings.Split(newChannel, "/")
if strutil.ListContains(channelRisks, p[0]) && ch.Track != "" {
// risk/branch inherits the track if any
return trackPrefix + newChannel, nil
}
if newChannel != track && !strings.HasPrefix(newChannel, trackPrefix) {
// the track is pinned
return "", ErrPinnedTrackSwitch
}
return newChannel, nil
}