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
81 changes: 81 additions & 0 deletions .github/workflows/pro_tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Pro Tests

on:
workflow_dispatch:
push:
paths-ignore:
- '**.md'
schedule:
- cron: "0 0 */2 * *"
workflow_run:
workflows: ["CLA check"]
types:
- completed

jobs:
real-archive-tests:
name: Real Archive Tests
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
# Do not change to newer releases as "fips" may not be available there.
runs-on: ubuntu-20.04
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 real archives.
go test ./internal/archive/ --real-pro-archive

spread-tests:
name: Spread tests
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3

- uses: actions/checkout@v3
with:
repository: snapcore/spread
path: _spread

- uses: actions/setup-go@v3
with:
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 tests/pro-archives
2 changes: 1 addition & 1 deletion .github/workflows/spread.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
- name: Build and run spread
run: |
(cd _spread/cmd/spread && go build)
_spread/cmd/spread/spread -v focal jammy mantic noble
_spread/cmd/spread/spread -v
15 changes: 15 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,18 @@ 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
run: |
go test ./internal/archive/ --real-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>).

## Support for Pro archives
> [!IMPORTANT]
> To chisel a Pro package you need to have a Pro-enabled host.

letFunny marked this conversation as resolved.
Show resolved Hide resolved
To fetch and install slices from Ubuntu Pro packages, the Pro archive has to be
defined with the `archives.<name>.pro` field in `chisel.yaml`:


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

The following Pro archives are currently supported:

| `pro` value | Archive URL |
|--------------|--------------------------------------------|
| fips | https://esm.ubuntu.com/fips/ubuntu |
| fips-updates | https://esm.ubuntu.com/fips-updates/ubuntu |
| esm-apps | https://esm.ubuntu.com/apps/ubuntu |
| esm-infra | https://esm.ubuntu.com/infra/ubuntu |

If the system is using the [Pro client](https://ubuntu.com/pro/tutorial), and the
services are enabled, the credentials will be automatically picked up from
`/etc/apt/auth.conf.d/`. However, the default permissions of the credentials file
need to be changed so that Chisel can read it. Example:
```shell
sudo pro enable esm-infra

sudo setfacl -m u:$USER:r /etc/apt/auth.conf.d/90ubuntu-advantage
# or, alternatively,
sudo chmod u+r /etc/apt/auth.conf.d/90ubuntu-advantage
```

The location of the credentials can be configured using the environment variable
`CHISEL_AUTH_DIR`.

## 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("Archive %q ignored: credentials not found", archiveName)
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
109 changes: 97 additions & 12 deletions internal/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Options struct {
Arch string
Suites []string
Components []string
Pro string
CacheDir string
PubKeys []*packet.PublicKey
}
Expand Down Expand Up @@ -77,6 +78,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 +150,54 @@ func (a *ubuntuArchive) Info(pkg string) (*PackageInfo, error) {
const ubuntuURL = "http://archive.ubuntu.com/ubuntu/"
const ubuntuPortsURL = "http://ports.ubuntu.com/ubuntu-ports/"

const (
ProFIPS = "fips"
ProFIPSUpdates = "fips-updates"
ProApps = "esm-apps"
ProInfra = "esm-infra"
)

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

func archiveURL(pro, arch string) (string, *credentials, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Function looks good now, thanks.

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

if arch == "amd64" || arch == "i386" {
return ubuntuURL, nil, nil
}
return ubuntuPortsURL, nil, nil
}

func openUbuntu(options *Options) (Archive, error) {
if len(options.Components) == 0 {
return nil, fmt.Errorf("archive options missing components")
Expand All @@ -158,12 +209,19 @@ func openUbuntu(options *Options) (Archive, error) {
return nil, fmt.Errorf("archive options missing version")
}

baseURL, creds, err := archiveURL(options.Pro, options.Arch)
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 +242,11 @@ 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.
break
}
err = index.checkComponents(options.Components)
if err != nil {
return nil, err
Expand All @@ -201,7 +264,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.displayName(), index.version, index.suite)
reader, err := index.fetch("InRelease", "", fetchDefault)
if err != nil {
return err
Expand Down Expand Up @@ -235,12 +298,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 +321,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.displayName(), index.version, index.suite, index.component)
reader, err := index.fetch(packagesPath+".gz", digest, fetchBulk)
if err != nil {
return err
Expand All @@ -270,6 +335,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 +371,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 +384,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 +401,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 +441,10 @@ func sectionPackageInfo(section control.Section) *PackageInfo {
SHA256: section.Get("SHA256"),
}
}

func (index *ubuntuIndex) displayName() string {
if index.archive.options.Pro == "" {
return index.label
}
return index.label + " " + index.archive.options.Pro + " (pro)"
}
Loading
Loading