Skip to content

Commit

Permalink
sources/fedora-http: Unpack Fedora from ISO
Browse files Browse the repository at this point in the history
Signed-off-by: Din Music <[email protected]>
  • Loading branch information
MusicDin committed Feb 10, 2025
1 parent 0069840 commit cd5ad8a
Showing 1 changed file with 178 additions and 18 deletions.
196 changes: 178 additions & 18 deletions sources/fedora-http.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sources

import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
Expand All @@ -10,49 +11,87 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"

"github.com/canonical/lxd-imagebuilder/shared"
)

type fedora struct {
common
commonRHEL
}

// Run downloads a container base image and unpacks it and its layers.
func (s *fedora) Run() error {
baseURL := fmt.Sprintf("%s/packages/Fedora-Container-Base", s.definition.Source.URL)
// For backwards compatibility, fallback to manual URL construction
// when the release version is equal or less than 40.
relNum, err := strconv.Atoi(s.definition.Image.Release)
if err == nil && relNum <= 40 {
baseURL := fmt.Sprintf("%s/packages/Fedora-Container-Base", s.definition.Source.URL)

// Get latest build
build, err := s.getLatestBuild(baseURL, s.definition.Image.Release)
if err != nil {
return fmt.Errorf("Failed to get latest build: %w", err)
}

// Get latest build
build, err := s.getLatestBuild(baseURL, s.definition.Image.Release)
if err != nil {
return fmt.Errorf("Failed to get latest build: %w", err)
fname := fmt.Sprintf("Fedora-Container-Base-%s-%s.%s.tar.xz", s.definition.Image.Release, build, s.definition.Image.ArchitectureMapped)

// Download image
sourceURL := fmt.Sprintf("%s/%s/%s/images/%s", baseURL, s.definition.Image.Release, build, fname)

fpath, err := s.DownloadHash(s.definition.Image, sourceURL, "", nil)
if err != nil {
return fmt.Errorf("Failed to download %q: %w", sourceURL, err)
}

s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image")

// Unpack the base image
err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir)
if err != nil {
return fmt.Errorf("Failed to unpack %q: %w", filepath.Join(fpath, fname), err)
}

s.logger.Info("Unpacking layers")

// Unpack the rest of the image (/bin, /sbin, /usr, etc.)
err = s.unpackLayers(s.rootfsDir)
if err != nil {
return fmt.Errorf("Failed to unpack: %w", err)
}

return nil
}

fname := fmt.Sprintf("Fedora-Container-Base-%s-%s.%s.tar.xz",
s.definition.Image.Release, build, s.definition.Image.ArchitectureMapped)
// Otherwise, get the ISO image URL from the available releases.
sourceURL, checksum, err := s.getDownloadURL(s.definition.Image.Release, s.definition.Image.Variant, s.definition.Image.ArchitectureMapped, ".iso")
if err != nil {
return err
}

// Download image
sourceURL := fmt.Sprintf("%s/%s/%s/images/%s", baseURL, s.definition.Image.Release, build, fname)
if checksum == "" {
return fmt.Errorf("Checksum not found for %q", sourceURL)
}

fpath, err := s.DownloadHash(s.definition.Image, sourceURL, "", nil)
if err != nil {
return fmt.Errorf("Failed to download %q: %w", sourceURL, err)
}

s.logger.WithField("file", filepath.Join(fpath, fname)).Info("Unpacking image")
fname := filepath.Join(fpath, filepath.Base(sourceURL))

// Unpack the base image
err = shared.Unpack(filepath.Join(fpath, fname), s.rootfsDir)
err = shared.VerifyChecksum(fpath, checksum, sha256.New())
if err != nil {
return fmt.Errorf("Failed to unpack %q: %w", filepath.Join(fpath, fname), err)
return fmt.Errorf("Failed to verify checksum: %w", err)
}

s.logger.Info("Unpacking layers")
s.logger.WithField("file", fname).Info("Unpacking image")

// Unpack the rest of the image (/bin, /sbin, /usr, etc.)
err = s.unpackLayers(s.rootfsDir)
// Unpack the base image
err = s.unpackISO(fname, s.rootfsDir, s.isoRunner)
if err != nil {
return fmt.Errorf("Failed to unpack: %w", err)
return fmt.Errorf("Failed to unpack %q: %w", fname, err)
}

return nil
Expand Down Expand Up @@ -174,3 +213,124 @@ func (s *fedora) getLatestBuild(URL string, release string) (string, error) {
// Return latest build
return matches[len(matches)-1], nil
}

// getDownloadURL fetches JSON representation of current releases and
// extracts the download URL matching the given release, variant, arch,
// and filetype (.tar.xz, .iso, etc.).
func (s *fedora) getDownloadURL(release string, variant string, arch string, fileType string) (url string, checksum string, err error) {
releasesURL := "https://fedoraproject.org/releases.json"
s.logger.Infof("Querying %q for list of available releases", releasesURL)

// Fetch available releases.
resp, err := http.Get(releasesURL)
if err != nil {
return "", "", fmt.Errorf("Failed to fetch releases from %q: %w", releasesURL, err)
}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("Unexpected status code %q from %q: %w", resp.StatusCode, releasesURL, err)
}

// Parse JSON response.
var result []struct {
Release string `json:"version"`
Arch string `json:"arch"`
Variant string `json:"variant"`
Subvariant string `json:"subvariant"`
URL string `json:"link"`
SHA256 string `json:"sha256"`
SizeBytes string `json:"size"`
}

err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return "", "", fmt.Errorf("Failed to parse JSON response: %w", err)
}

// Variant mapping.
// Default to variant "Container" and subvariant "Container_Base".
var subvariant string
if variant == "default" || variant == "cloud" {
variant = "Server"
subvariant = "Server"
}

// Iterate over response items and find the matching image.
for _, item := range result {
if item.Release != release || item.Arch != arch || item.Variant != variant {
continue
}

if subvariant != "" && item.Subvariant != subvariant {
continue
}

if !strings.HasSuffix(item.URL, fileType) {
continue
}

// Matching URL found.
return item.URL, item.SHA256, nil
}

return "", "", fmt.Errorf("Failed to find download URL for release %q, variant %q, arch %q, and file type %q", release, variant, arch, fileType)
}

func (s *fedora) isoRunner(gpgkeys string) error {
err := shared.RunScript(s.ctx, fmt.Sprintf(`#!/bin/sh
set -eux
# Create required files
touch /etc/mtab /etc/fstab
dnf_args=""
mkdir -p /etc/yum.repos.d
# Add cdrom repo
cat <<- EOF > /etc/yum.repos.d/cdrom.repo
[cdrom]
name=Install CD-ROM
baseurl=file:///mnt/cdrom
enabled=0
EOF
GPG_KEYS="%s"
gpg_keys_official="file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-%[2]s-primary"
if [ -n "${GPG_KEYS}" ]; then
echo gpgcheck=1 >> /etc/yum.repos.d/cdrom.repo
echo gpgkey=${gpg_keys_official} ${GPG_KEYS} >> /etc/yum.repos.d/cdrom.repo
else
echo gpgcheck=0 >> /etc/yum.repos.d/cdrom.repo
fi
dnf_args="--disablerepo=* --enablerepo=cdrom"
# Newest install.img doesnt have rpm installed,
# so first install rpm.
if ! command -v rpmkeys; then
cd /mnt/cdrom/Packages
dnf ${dnf_args} -y install rpm
fi
pkgs="basesystem fedora-release dnf"
# Create a minimal rootfs
mkdir /rootfs
dnf ${dnf_args} --installroot=/rootfs --releasever=%[2]s -y install ${pkgs}
rm -rf /rootfs/var/cache/yum
rm -rf /rootfs/var/cache/dnf
rm -rf /etc/yum.repos.d/cdrom.repo
# Remove all files in mnt packages
rm -rf /mnt/cdrom
`, gpgkeys, s.definition.Image.Release))

if err != nil {
return fmt.Errorf("Failed to run script: %w", err)
}

return nil
}

0 comments on commit cd5ad8a

Please sign in to comment.