diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 7c90a41d..6e7d054e 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -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/" @@ -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) { 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) { @@ -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. + continue } + if err != nil { + return nil, err + } + return archive, nil } - - return archive, nil + return nil, errNotFound } func (index *ubuntuIndex) fetchRelease() error { diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 90e3f004..9045c24c 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -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