Skip to content

Commit ecd8b8a

Browse files
authored
feat: support unmaintained and unstable Ubuntu releases (#238)
This commits adds maintenance status to a release as the top-level field maintenance in chisel.yaml. The dates there specify the lifecycle of the release in regards to its support status, with and without Ubuntu Pro subscriptions. This enables Chisel to fail if the release and its combination of archives is no longer supported officially and it may lead to security problems. For example, interim releases like mantic and no longer officially supported, and LTSs like focal are only maintained when using ESM which needs an Ubuntu Pro subscription. If no maintained archive is available Chisel will fail, but the user can continue execution by passing --ignore=unmaintained as a CLI flag. When the dates are into the future it indicates instead that the release is unstable. The user can choose to continue the execution by passing --ignore=unstable as a CLI flag. Lastly, this commits also allows Chisel to work with interim releases by using the dates described above to change the archive's URL from archive.ubuntu.com to old-releases.ubuntu.com when needed. Compatibility-wise, Chisel has a list of releases with their default dates so it continues to work with existing chisel.yaml. There will be a window when maintenance is optional for these releases and chisel-releases will be updated. Then, the field will be mandatory. IMPORTANT: This new version of Chisel will fail to cut Ubuntu 20.04 when Ubuntu Pro is not enabled because the standard archive is unmaintained. Users need to add --ignore=unmaintained or move to a more recent release or use the Ubuntu Pro archives.
1 parent aca3b32 commit ecd8b8a

File tree

18 files changed

+1375
-78
lines changed

18 files changed

+1375
-78
lines changed

cmd/chisel/cmd_cut.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package main
22

33
import (
4+
"fmt"
5+
"time"
6+
47
"github.com/jessevdk/go-flags"
58

69
"github.com/canonical/chisel/internal/archive"
@@ -22,12 +25,14 @@ var cutDescs = map[string]string{
2225
"release": "Chisel release name or directory (e.g. ubuntu-22.04)",
2326
"root": "Root for generated content",
2427
"arch": "Package architecture",
28+
"ignore": "Conditions to ignore (e.g. unmaintained, unstable)",
2529
}
2630

2731
type cmdCut struct {
2832
Release string `long:"release" value-name:"<dir>"`
2933
RootDir string `long:"root" value-name:"<dir>" required:"yes"`
3034
Arch string `long:"arch" value-name:"<arch>"`
35+
Ignore string `long:"ignore" choice:"unmaintained" choice:"unstable" value-name:"<cond>"`
3136

3237
Positional struct {
3338
SliceRefs []string `positional-arg-name:"<slice names>" required:"yes"`
@@ -57,6 +62,16 @@ func (cmd *cmdCut) Execute(args []string) error {
5762
return err
5863
}
5964

65+
if time.Now().Before(release.Maintenance.Standard) {
66+
if cmd.Ignore == "unstable" {
67+
logf(`Warning: This release is in the "unstable" maintenance status. ` +
68+
`See https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance to be safe`)
69+
} else {
70+
return fmt.Errorf(`this release is in the "unstable" maintenance status, ` +
71+
`see https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details`)
72+
}
73+
}
74+
6075
selection, err := setup.Select(release, sliceKeys)
6176
if err != nil {
6277
return err
@@ -73,6 +88,8 @@ func (cmd *cmdCut) Execute(args []string) error {
7388
Pro: archiveInfo.Pro,
7489
CacheDir: cache.DefaultDir("chisel"),
7590
PubKeys: archiveInfo.PubKeys,
91+
Maintained: archiveInfo.Maintained,
92+
OldRelease: archiveInfo.OldRelease,
7693
})
7794
if err != nil {
7895
if err == archive.ErrCredentialsNotFound {
@@ -84,6 +101,25 @@ func (cmd *cmdCut) Execute(args []string) error {
84101
archives[archiveName] = openArchive
85102
}
86103

104+
hasMaintainedArchive := false
105+
for _, archive := range archives {
106+
if archive.Options().Maintained {
107+
hasMaintainedArchive = true
108+
break
109+
}
110+
}
111+
if !hasMaintainedArchive {
112+
if cmd.Ignore == "unmaintained" {
113+
logf(`Warning: No archive has "maintained" maintenance status. ` +
114+
`Consider the different Ubuntu Pro subcriptions to be safe. ` +
115+
`See https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details.`)
116+
} else {
117+
return fmt.Errorf(`no archive has "maintained" maintenance status, ` +
118+
`consider the different Ubuntu Pro subcriptions to be safe, ` +
119+
`see https://documentation.ubuntu.com/chisel/en/latest/reference/chisel-releases/chisel.yaml/#maintenance for details`)
120+
}
121+
}
122+
87123
err = slicer.Run(&slicer.RunOptions{
88124
Selection: selection,
89125
Archives: archives,

cmd/chisel/cmd_debug_check_release_archives.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func (cmd *cmdDebugCheckReleaseArchives) Execute(args []string) error {
7373
Pro: archiveInfo.Pro,
7474
CacheDir: cache.DefaultDir("chisel"),
7575
PubKeys: archiveInfo.PubKeys,
76+
Maintained: archiveInfo.Maintained,
77+
OldRelease: archiveInfo.OldRelease,
7678
})
7779
if err == archive.ErrCredentialsNotFound {
7880
logf("Archive %q ignored: credentials not found\n", archiveName)

cmd/chisel/cmd_debug_check_release_archives_test.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -542,19 +542,7 @@ func (s *ChiselSuite) TestRun(c *C) {
542542
// makeChiselYaml returns a valid chisel.yaml that contains the archives
543543
// supplied.
544544
func makeChiselYaml(archives []string) string {
545-
archiveKey := testutil.PGPKeys["key-ubuntu-2018"]
546-
rawChiselYaml := testutil.Reindent(`
547-
format: v1
548-
archives:
549-
ubuntu:
550-
version: 22.04
551-
components: [main, universe]
552-
suites: [jammy]
553-
public-keys: [test-key]
554-
public-keys:
555-
test-key:
556-
id: ` + archiveKey.ID + `
557-
armor: |` + "\n" + testutil.PrefixEachLine(archiveKey.PubKeyArmor, "\t\t\t\t\t\t"))
545+
rawChiselYaml := testutil.Reindent(testutil.DefaultChiselYaml)
558546

559547
chiselYaml := map[string]any{}
560548
err := yaml.Unmarshal([]byte(rawChiselYaml), chiselYaml)
@@ -578,7 +566,9 @@ func makeChiselYaml(archives []string) string {
578566
if err != nil {
579567
panic(err)
580568
}
581-
return string(bs)
569+
out := string(bs)
570+
// Maintenance dates get marshaled as <date>T00:00:00Z by default.
571+
return strings.ReplaceAll(out, "T00:00:00Z", "")
582572
}
583573

584574
func deepCopyYAML(src map[string]any) map[string]any {

cmd/chisel/cmd_info_test.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,23 +137,8 @@ var infoTests = []infoTest{{
137137
err: `no slice definitions found for: "foo_bar_foo", "a_b", "7_c", "a_b c", "a_b x_y"`,
138138
}}
139139

140-
var testKey = testutil.PGPKeys["key1"]
141-
142-
var defaultChiselYaml = `
143-
format: v1
144-
archives:
145-
ubuntu:
146-
version: 22.04
147-
components: [main, universe]
148-
suites: [jammy]
149-
public-keys: [test-key]
150-
public-keys:
151-
test-key:
152-
id: ` + testKey.ID + `
153-
armor: |` + "\n" + testutil.PrefixEachLine(testKey.PubKeyArmor, "\t\t\t\t\t\t")
154-
155140
var infoRelease = map[string]string{
156-
"chisel.yaml": string(defaultChiselYaml),
141+
"chisel.yaml": string(testutil.DefaultChiselYaml),
157142
"slices/mypkg1.yaml": `
158143
package: mypkg1
159144
essential:

internal/archive/archive.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ type Options struct {
4040
Pro string
4141
CacheDir string
4242
PubKeys []*packet.PublicKey
43+
// Maintained is set when the archive is still being updated.
44+
Maintained bool
45+
// OldRelease is set for Ubuntu releases which are moved from the regular
46+
// archive which happens after the release's end of life date.
47+
OldRelease bool
4348
}
4449

4550
func Open(options *Options) (Archive, error) {
@@ -149,6 +154,7 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) {
149154
}
150155

151156
const ubuntuURL = "http://archive.ubuntu.com/ubuntu/"
157+
const ubuntuOldReleasesURL = "http://old-releases.ubuntu.com/ubuntu/"
152158
const ubuntuPortsURL = "http://ports.ubuntu.com/ubuntu-ports/"
153159

154160
const (
@@ -179,7 +185,7 @@ var proArchiveInfo = map[string]struct {
179185
},
180186
}
181187

182-
func archiveURL(pro, arch string) (string, *credentials, error) {
188+
func archiveURL(pro, arch string, oldRelease bool) (string, *credentials, error) {
183189
if pro != "" {
184190
archiveInfo, ok := proArchiveInfo[pro]
185191
if !ok {
@@ -193,6 +199,10 @@ func archiveURL(pro, arch string) (string, *credentials, error) {
193199
return url, creds, nil
194200
}
195201

202+
if oldRelease {
203+
return ubuntuOldReleasesURL, nil, nil
204+
}
205+
196206
if arch == "amd64" || arch == "i386" {
197207
return ubuntuURL, nil, nil
198208
}
@@ -210,7 +220,7 @@ func openUbuntu(options *Options) (Archive, error) {
210220
return nil, fmt.Errorf("archive options missing version")
211221
}
212222

213-
baseURL, creds, err := archiveURL(options.Pro, options.Arch)
223+
baseURL, creds, err := archiveURL(options.Pro, options.Arch, options.OldRelease)
214224
if err != nil {
215225
return nil, err
216226
}

internal/archive/archive_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,31 @@ func (s *httpSuite) TestProArchives(c *C) {
491491
}
492492
}
493493

494+
func (s *httpSuite) TestOpenUnmaintainedArchives(c *C) {
495+
s.base = "http://old-releases.ubuntu.com/ubuntu/"
496+
s.prepareArchive("jammy", "22.04", "amd64", []string{"main", "universe"})
497+
498+
options := archive.Options{
499+
Label: "ubuntu",
500+
Version: "22.04",
501+
Arch: "amd64",
502+
Suites: []string{"jammy"},
503+
Components: []string{"main", "universe"},
504+
CacheDir: c.MkDir(),
505+
PubKeys: []*packet.PublicKey{s.pubKey},
506+
OldRelease: false,
507+
}
508+
509+
_, err := archive.Open(&options)
510+
// Fails when OldRelease is not set because it attempts to contact the
511+
// default ubuntu archive where the release is no longer available.
512+
c.Assert(err, Not(IsNil))
513+
514+
options.OldRelease = true
515+
_, err = archive.Open(&options)
516+
c.Assert(err, IsNil)
517+
}
518+
494519
type verifyArchiveReleaseTest struct {
495520
summary string
496521
pubKeys []*packet.PublicKey
@@ -642,6 +667,7 @@ type realArchiveTest struct {
642667
suites []string
643668
components []string
644669
pro string
670+
oldRelease bool
645671
archivePubKeys []*packet.PublicKey
646672
archs []string
647673
pkg string
@@ -651,6 +677,7 @@ type realArchiveTest struct {
651677
var realArchiveTests = []realArchiveTest{{
652678
name: "focal",
653679
version: "20.04",
680+
oldRelease: false,
654681
suites: []string{"focal"},
655682
components: []string{"main", "universe"},
656683
archivePubKeys: []*packet.PublicKey{keyUbuntu2018.PubKey},
@@ -659,6 +686,7 @@ var realArchiveTests = []realArchiveTest{{
659686
}, {
660687
name: "jammy",
661688
version: "22.04",
689+
oldRelease: false,
662690
suites: []string{"jammy"},
663691
components: []string{"main", "universe"},
664692
archivePubKeys: []*packet.PublicKey{keyUbuntu2018.PubKey},
@@ -667,11 +695,21 @@ var realArchiveTests = []realArchiveTest{{
667695
}, {
668696
name: "noble",
669697
version: "24.04",
698+
oldRelease: false,
670699
suites: []string{"noble"},
671700
components: []string{"main", "universe"},
672701
archivePubKeys: []*packet.PublicKey{keyUbuntu2018.PubKey},
673702
pkg: "hostname",
674703
path: "/usr/bin/hostname",
704+
}, {
705+
name: "mantic",
706+
version: "23.10",
707+
oldRelease: true,
708+
suites: []string{"mantic"},
709+
components: []string{"main", "universe"},
710+
archivePubKeys: []*packet.PublicKey{keyUbuntu2018.PubKey},
711+
pkg: "hostname",
712+
path: "/bin/hostname",
675713
}}
676714

677715
var proArchiveTests = []realArchiveTest{{
@@ -797,6 +835,7 @@ func (s *S) testOpenArchiveArch(c *C, test realArchiveTest, arch string) {
797835
CacheDir: c.MkDir(),
798836
Pro: test.pro,
799837
PubKeys: test.archivePubKeys,
838+
OldRelease: test.oldRelease,
800839
}
801840

802841
testArchive, err := archive.Open(&options)

internal/setup/setup.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path/filepath"
88
"slices"
99
"strings"
10+
"time"
1011

1112
"golang.org/x/crypto/openpgp/packet"
1213

@@ -17,9 +18,17 @@ import (
1718
// Release is a collection of package slices targeting a particular
1819
// distribution version.
1920
type Release struct {
20-
Path string
21-
Packages map[string]*Package
22-
Archives map[string]*Archive
21+
Path string
22+
Packages map[string]*Package
23+
Archives map[string]*Archive
24+
Maintenance *Maintenance
25+
}
26+
27+
type Maintenance struct {
28+
Standard time.Time
29+
Expanded time.Time
30+
Legacy time.Time
31+
EndOfLife time.Time
2332
}
2433

2534
// Archive is the location from which binary packages are obtained.
@@ -31,6 +40,11 @@ type Archive struct {
3140
Priority int
3241
Pro string
3342
PubKeys []*packet.PublicKey
43+
// Maintained is set when the archive is still being updated.
44+
Maintained bool
45+
// OldRelease is set for Ubuntu releases which are moved from the regular
46+
// archive which happens after the release's end of life date.
47+
OldRelease bool
3448
}
3549

3650
// Package holds a collection of slices that represent parts of themselves.

0 commit comments

Comments
 (0)