Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support "Pro" archives #167

Merged
merged 13 commits into from
Nov 15, 2024
2 changes: 2 additions & 0 deletions .github/workflows/spread.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
go-version: '>=1.17.0'

- name: Build and run spread
env:
PRO_TOKEN: ${{ secrets.PRO_TOKEN }}
run: |
(cd _spread/cmd/spread && go build)
_spread/cmd/spread/spread -v focal jammy mantic noble
43 changes: 43 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,46 @@ jobs:
with:
name: chisel-test-coverage.html
path: ./*.html

real-archive-tests:
# Do not change to newer releases as "fips" may not be available there.
runs-on: ubuntu-20.04
name: Real Archive Tests
steps:
- uses: actions/checkout@v3

- uses: actions/setup-go@v3
with:
go-version-file: 'go.mod'

- name: Run real archive tests
env:
PRO_TOKEN: ${{ secrets.PRO_TOKEN }}
run: |
set -ex

detach() {
sudo pro detach --assume-yes || true
sudo rm -f /etc/apt/auth.conf.d/90ubuntu-advantage
}
trap detach EXIT

# Attach pro token and enable services
sudo pro attach ${PRO_TOKEN} --no-auto-enable

# Cannot enable fips and fips-updates at the same time.
# Hack: enable fips, copy the credentials and then after enabling
# other services, add the credentials back.
sudo pro enable fips --assume-yes
sudo cp /etc/apt/auth.conf.d/90ubuntu-advantage /etc/apt/auth.conf.d/90ubuntu-advantage.fips-creds
# This will disable the fips service.
sudo pro enable fips-updates esm-apps esm-infra --assume-yes
# Add the fips credentials back.
sudo sh -c 'cat /etc/apt/auth.conf.d/90ubuntu-advantage.fips-creds >> /etc/apt/auth.conf.d/90ubuntu-advantage'
sudo rm /etc/apt/auth.conf.d/90ubuntu-advantage.fips-creds

# Make apt credentials accessible to USER.
sudo setfacl -m u:$USER:r /etc/apt/auth.conf.d/90ubuntu-advantage

# Run tests on Pro and non-Pro real archives.
go test ./internal/archive/ -v --real-archive --real-pro-archive
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,48 @@ provided packages and install only the desired slices into the *myrootfs*
folder, according to the slice definitions available in the
["ubuntu-22.04" chisel-releases branch](<https://github.com/canonical/chisel-releases/tree/ubuntu-22.04>).

## Chisel support for Pro archives
letFunny marked this conversation as resolved.
Show resolved Hide resolved

letFunny marked this conversation as resolved.
Show resolved Hide resolved
Chisel can also fetch and install packages from Ubuntu Pro archives. For this,
the archive has to be defined with the `archives.<archive>.pro` field in
letFunny marked this conversation as resolved.
Show resolved Hide resolved
chisel.yaml and its credentials have to be made available to Chisel.


```yaml
# chisel.yaml
format: v1
archives:
<archive-name>:
pro: <value>
...
...
```

Chisel currently supports the following Pro archives:

| `pro` value | Archive URL | Related Ubuntu Pro service |
letFunny marked this conversation as resolved.
Show resolved Hide resolved
| - | - | - |
| fips | https://esm.ubuntu.com/fips/ubuntu | fips |
| fips-updates | https://esm.ubuntu.com/fips-updates/ubuntu | fips-updates |
| apps | https://esm.ubuntu.com/apps/ubuntu | esm-apps |
| infra | https://esm.ubuntu.com/infra/ubuntu | esm-infra |
letFunny marked this conversation as resolved.
Show resolved Hide resolved

Authentication to Pro archives requires that the host is Pro or it is equipped
with the Pro credentials. By default, Chisel will support using credentials
from the `/etc/apt/auth.conf.d/` directory, but this location can be configured
letFunny marked this conversation as resolved.
Show resolved Hide resolved
using the environment variable `CHISEL_AUTH_DIR`. Note that Chisel must have
read permission for the necessary credentials files.

The format of the files is documented in the
letFunny marked this conversation as resolved.
Show resolved Hide resolved
[apt_auth.conf(5)](https://manpages.debian.org/testing/apt/apt_auth.conf.5.en.html)
man page. Below is a snippet of the `/etc/apt/auth.conf.d/90ubuntu-advantage`
file from a host with the `fips-updates` and `infra` archives enabled:

```
machine esm.ubuntu.com/infra/ubuntu/ login bearer password <infra-token>
machine esm.ubuntu.com/fips-updates/ubuntu/ login bearer password <fips-updates-token>
```

## Reference

### Chisel releases
Expand Down
5 changes: 5 additions & 0 deletions cmd/chisel/cmd_cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ func (cmd *cmdCut) Execute(args []string) error {
Arch: cmd.Arch,
Suites: archiveInfo.Suites,
Components: archiveInfo.Components,
Pro: archiveInfo.Pro,
CacheDir: cache.DefaultDir("chisel"),
PubKeys: archiveInfo.PubKeys,
})
if err != nil {
if err == archive.ErrCredentialsNotFound {
logf("Ignoring archive %q (credentials not found)...", archiveName)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
continue
}
return err
}
archives[archiveName] = openArchive
Expand Down
1 change: 1 addition & 0 deletions cmd/chisel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ func run() error {
deb.SetLogger(log.Default())
setup.SetLogger(log.Default())
slicer.SetLogger(log.Default())
SetLogger(log.Default())

parser := Parser()
xtra, err := parser.Parse()
Expand Down
110 changes: 98 additions & 12 deletions internal/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/canonical/chisel/internal/control"
"github.com/canonical/chisel/internal/deb"
"github.com/canonical/chisel/internal/pgputil"
"github.com/canonical/chisel/internal/setup"
letFunny marked this conversation as resolved.
Show resolved Hide resolved
)

type Archive interface {
Expand All @@ -36,6 +37,7 @@ type Options struct {
Arch string
Suites []string
Components []string
Pro string
CacheDir string
PubKeys []*packet.PublicKey
}
Expand Down Expand Up @@ -77,6 +79,8 @@ type ubuntuArchive struct {
indexes []*ubuntuIndex
cache *cache.Cache
pubKeys []*packet.PublicKey
baseURL string
creds *credentials
}

type ubuntuIndex struct {
Expand Down Expand Up @@ -147,6 +151,39 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) {
const ubuntuURL = "http://archive.ubuntu.com/ubuntu/"
const ubuntuPortsURL = "http://ports.ubuntu.com/ubuntu-ports/"

var proArchiveInfo = map[string]struct {
BaseURL, Label string
}{
setup.ProFIPS: {
BaseURL: "https://esm.ubuntu.com/fips/ubuntu/",
Label: "UbuntuFIPS",
},
setup.ProFIPSUpdates: {
BaseURL: "https://esm.ubuntu.com/fips-updates/ubuntu/",
Label: "UbuntuFIPSUpdates",
},
setup.ProApps: {
BaseURL: "https://esm.ubuntu.com/apps/ubuntu/",
Label: "UbuntuESMApps",
},
setup.ProInfra: {
BaseURL: "https://esm.ubuntu.com/infra/ubuntu/",
Label: "UbuntuESM",
},
}

// archiveURL returns the archive base URL depending on the "pro" value and
// selected architecture "arch".
letFunny marked this conversation as resolved.
Show resolved Hide resolved
func archiveURL(pro, arch string) string {
if pro != "" {
return proArchiveInfo[pro].BaseURL
}
if arch == "amd64" || arch == "i386" {
return ubuntuURL
}
return ubuntuPortsURL
}

func openUbuntu(options *Options) (Archive, error) {
if len(options.Components) == 0 {
return nil, fmt.Errorf("archive options missing components")
Expand All @@ -157,13 +194,30 @@ func openUbuntu(options *Options) (Archive, error) {
if len(options.Version) == 0 {
return nil, fmt.Errorf("archive options missing version")
}
if options.Pro != "" {
if _, ok := proArchiveInfo[options.Pro]; !ok {
return nil, fmt.Errorf("invalid pro value: %q", options.Pro)
}
}

baseURL := archiveURL(options.Pro, options.Arch)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
var creds *credentials
if options.Pro != "" {
var err error
creds, err = findCredentials(baseURL)
letFunny marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand All @@ -184,6 +238,13 @@ func openUbuntu(options *Options) (Archive, error) {
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.
logf("Warning: ignoring %s %s %s suite (unsupported arch %s)...",
letFunny marked this conversation as resolved.
Show resolved Hide resolved
index.proSuffixedLabel(), index.version, index.suite, options.Arch)
break
}
err = index.checkComponents(options.Components)
if err != nil {
return nil, err
Expand All @@ -201,7 +262,7 @@ func openUbuntu(options *Options) (Archive, error) {
}

func (index *ubuntuIndex) fetchRelease() error {
logf("Fetching %s %s %s suite details...", index.label, index.version, index.suite)
logf("Fetching %s %s %s suite details...", index.proSuffixedLabel(), index.version, index.suite)
reader, err := index.fetch("InRelease", "", fetchDefault)
if err != nil {
return err
Expand Down Expand Up @@ -235,12 +296,14 @@ func (index *ubuntuIndex) fetchRelease() error {
if err != nil {
return fmt.Errorf("cannot parse InRelease file: %v", err)
}
section := ctrl.Section("Ubuntu")
// Parse the appropriate section for the type of archive.
label := "Ubuntu"
if index.archive.options.Pro != "" {
label = proArchiveInfo[index.archive.options.Pro].Label
}
section := ctrl.Section(label)
if section == nil {
section = ctrl.Section("UbuntuProFIPS")
if section == nil {
return fmt.Errorf("corrupted archive InRelease file: no Ubuntu section")
}
return fmt.Errorf("corrupted archive InRelease file: no %s section", label)
}
logf("Release date: %s", section.Get("Date"))

Expand All @@ -256,7 +319,7 @@ func (index *ubuntuIndex) fetchIndex() error {
return fmt.Errorf("%s is missing from %s %s component digests", packagesPath, index.suite, index.component)
}

logf("Fetching index for %s %s %s %s component...", index.label, index.version, index.suite, index.component)
logf("Fetching index for %s %s %s %s component...", index.proSuffixedLabel(), index.version, index.suite, index.component)
reader, err := index.fetch(packagesPath+".gz", digest, fetchBulk)
if err != nil {
return err
Expand All @@ -270,6 +333,17 @@ func (index *ubuntuIndex) fetchIndex() error {
return nil
}

// supportsArch returns true if the Architectures field in the index release
// contains "arch". Per the Debian wiki [1], index release files should list the
// supported architectures in the "Architectures" field.
// The "ubuntuURL" archive only supports the amd64 and i386 architectures
// whereas the "ubuntuPortsURL" one supports the rest. But each of them
// (faultly) specifies all those architectures in their InRelease files.
// Reference: [1] https://wiki.debian.org/DebianRepository/Format#Architectures
func (index *ubuntuIndex) supportsArch(arch string) bool {
return strings.Contains(index.release.Get("Architectures"), arch)
}

func (index *ubuntuIndex) checkComponents(components []string) error {
releaseComponents := strings.Fields(index.release.Get("Components"))
for _, c1 := range components {
Expand All @@ -295,10 +369,7 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea
return nil, err
}

baseURL := ubuntuURL
if index.arch != "amd64" && index.arch != "i386" {
baseURL = ubuntuPortsURL
}
baseURL, creds := index.archive.baseURL, index.archive.creds

var url string
if strings.HasPrefix(suffix, "pool/") {
Expand All @@ -311,6 +382,9 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea
if err != nil {
return nil, fmt.Errorf("cannot create HTTP request: %v", err)
}
if creds != nil && !creds.Empty() {
req.SetBasicAuth(creds.Username, creds.Password)
}
var resp *http.Response
if flags&fetchBulk != 0 {
resp, err = bulkDo(req)
Expand All @@ -325,7 +399,9 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea
switch resp.StatusCode {
case 200:
// ok
case 401, 404:
case 401:
return nil, fmt.Errorf("cannot fetch from %q: unauthorized", index.label)
case 404:
return nil, fmt.Errorf("cannot find archive data")
default:
return nil, fmt.Errorf("error from archive: %v", resp.Status)
Expand Down Expand Up @@ -363,3 +439,13 @@ func sectionPackageInfo(section control.Section) *PackageInfo {
SHA256: section.Get("SHA256"),
}
}

// proSuffixedLabel adds "<pro value> (pro)" suffix to the label and returns it
// if the archive is specified with pro value. Otherwise, it returns the
// original label.
func (index *ubuntuIndex) proSuffixedLabel() string {
letFunny marked this conversation as resolved.
Show resolved Hide resolved
if index.archive.options.Pro == "" {
return index.label
}
return index.label + " " + index.archive.options.Pro + " (pro)"
}
Loading
Loading