Skip to content
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
135 changes: 79 additions & 56 deletions internal/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,47 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) {
return info, nil
}

// setupIndexes fetches the release and package indexes of every suite and
// component, registering the resulting indexes on the archive.
func (a *ubuntuArchive) setupIndexes() error {
for _, suite := range a.options.Suites {
var release control.Section
for _, component := range a.options.Components {
index := &ubuntuIndex{
label: a.options.Label,
version: a.options.Version,
arch: a.options.Arch,
suite: suite,
component: component,
release: release,
archive: a,
}
if release == nil {
err := index.fetchRelease()
if err != nil {
return err
}
release = index.release
if !index.supportsArch(a.options.Arch) {
// Release does not support the specified architecture, do
// not add any of its indexes.
break
}
err = index.checkComponents(a.options.Components)
if err != nil {
return err
}
}
err := index.fetchIndex()
if err != nil {
return err
}
a.indexes = append(a.indexes, index)
}
}
return nil
}

const ubuntuURL = "http://archive.ubuntu.com/ubuntu/"
const ubuntuOldReleasesURL = "http://old-releases.ubuntu.com/ubuntu/"
const ubuntuPortsURL = "http://ports.ubuntu.com/ubuntu-ports/"
Expand Down Expand Up @@ -189,28 +230,37 @@ var proArchiveInfo = map[string]struct {
},
}

func archiveURL(pro, arch string, oldRelease bool) (string, *credentials, error) {
// candidateArchiveURLs returns the candidate base URLs of the archive, in order of
// preference, and the credentials used to access them, if any.
//
// Ubuntu releases are moved from the regular archive to
// old-releases.ubuntu.com after their end of life, but not immediately:
// until the move happens the release is only available from the regular
// archive. For such releases both archives are returned, so that the
// caller can fall back to the regular one when the release is not found
// in the old-releases one.
func candidateArchiveURLs(pro, arch string, oldRelease bool) ([]string, *credentials, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Much nicer, thanks.

if pro != "" {
archiveInfo, ok := proArchiveInfo[pro]
if !ok {
return "", nil, fmt.Errorf("invalid pro value: %q", pro)
return nil, nil, fmt.Errorf("invalid pro value: %q", pro)
}
url := archiveInfo.BaseURL
creds, err := findCredentials(url)
if err != nil {
return "", nil, err
return nil, nil, err
}
return url, creds, nil
return []string{url}, creds, nil
}

if oldRelease {
return ubuntuOldReleasesURL, nil, nil
current := ubuntuURL
if arch != "amd64" && arch != "i386" {
current = ubuntuPortsURL
}

if arch == "amd64" || arch == "i386" {
return ubuntuURL, nil, nil
if oldRelease {
return []string{ubuntuOldReleasesURL, current}, nil, nil
}
return ubuntuPortsURL, nil, nil
return []string{current}, nil, nil
}

func openUbuntu(options *Options) (Archive, error) {
Expand All @@ -224,58 +274,31 @@ func openUbuntu(options *Options) (Archive, error) {
return nil, fmt.Errorf("archive options missing version")
}

baseURL, creds, err := archiveURL(options.Pro, options.Arch, options.OldRelease)
candidates, creds, err := candidateArchiveURLs(options.Pro, options.Arch, options.OldRelease)
if err != nil {
return nil, err
}

archive := &ubuntuArchive{
options: *options,
cache: &cache.Cache{
Dir: options.CacheDir,
},
pubKeys: options.PubKeys,
baseURL: baseURL,
creds: creds,
}

for _, suite := range options.Suites {
var release control.Section
for _, component := range options.Components {
index := &ubuntuIndex{
label: options.Label,
version: options.Version,
arch: options.Arch,
suite: suite,
component: component,
release: release,
archive: archive,
}
if release == nil {
err := index.fetchRelease()
if err != nil {
return nil, err
}
release = index.release
if !index.supportsArch(options.Arch) {
// Release does not support the specified architecture, do
// not add any of its indexes.
break
}
err = index.checkComponents(options.Components)
if err != nil {
return nil, err
}
}
err := index.fetchIndex()
if err != nil {
return nil, err
}
archive.indexes = append(archive.indexes, index)
// Try the candidate archives in order until the release is found.
for _, baseURL := range candidates {
archive := &ubuntuArchive{
options: *options,
cache: &cache.Cache{Dir: options.CacheDir},
pubKeys: options.PubKeys,
baseURL: baseURL,
creds: creds,
}
err := archive.setupIndexes()
if err == errNotFound {
// Release not in this archive, try the next candidate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment is not required, as the code is literally spelling it out:

The Indexes were errNotFound, so continue to the next candidates' baseURL.

continue
}
if err != nil {
return nil, err
}
return archive, nil
}

return archive, nil
return nil, errNotFound
}

func (index *ubuntuIndex) fetchRelease() error {
Expand Down
76 changes: 76 additions & 0 deletions internal/archive/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,82 @@ func (s *httpSuite) TestOpenUnmaintainedArchives(c *C) {
c.Assert(err, IsNil)
}

func (s *httpSuite) TestOpenOldReleaseFallback(c *C) {
s.prepareArchive("plucky", "25.04", "amd64", []string{"main"})

// The old-releases mirror 404s the release (it has not been physically
// moved yet); the current archive serves the prepared content.
do := func(req *http.Request) (*http.Response, error) {
if strings.HasPrefix(req.URL.String(), "http://old-releases.ubuntu.com/ubuntu/") {
s.requestResults = append(s.requestResults, requestResult{path: req.URL.Path, status: 404})
return &http.Response{
Body: io.NopCloser(strings.NewReader("")),
StatusCode: 404,
}, nil
}
return s.Do(req)
}
restoreDo := archive.FakeDo(do)
defer restoreDo()

options := archive.Options{
Label: "ubuntu",
Version: "25.04",
Arch: "amd64",
Suites: []string{"plucky"},
Components: []string{"main"},
CacheDir: c.MkDir(),
PubKeys: []*packet.PublicKey{s.pubKey},
OldRelease: true,
}

testArchive, err := archive.Open(&options)
c.Assert(err, IsNil)

_, _, err = testArchive.Fetch("mypkg1")
c.Assert(err, IsNil)

// Exactly one 404 (the InRelease fetch from old-releases); all
// subsequent requests must be served by the current archive.
oldReleasesHits := 0
for _, r := range s.requestResults {
if r.status == 404 {
oldReleasesHits++
}
}
c.Assert(oldReleasesHits, Equals, 1)
}

func (s *httpSuite) TestOpenOldReleaseNotFound(c *C) {
// No candidate archive distributes the release: accept requests from
// any host and 404 them all.
s.base = ""
s.status = 404

options := archive.Options{
Label: "ubuntu",
Version: "25.04",
Arch: "amd64",
Suites: []string{"plucky"},
Components: []string{"main"},
CacheDir: c.MkDir(),
PubKeys: []*packet.PublicKey{s.pubKey},
OldRelease: true,
}

_, err := archive.Open(&options)
c.Assert(err, ErrorMatches, "cannot find archive data")

// Both candidates must have been tried, one InRelease fetch each.
suites := 0
for _, r := range s.requestResults {
if strings.HasSuffix(r.path, "/dists/plucky/InRelease") {
suites++
}
}
c.Assert(suites, Equals, 2)
}

type verifyArchiveReleaseTest struct {
summary string
pubKeys []*packet.PublicKey
Expand Down
Loading