Skip to content

Commit

Permalink
extensions: Add support for hotfixes-$variant.yaml
Browse files Browse the repository at this point in the history
We have a bug in https://issues.redhat.com/browse/OCPBUGS-7275
where we need an updated rpm-ostree on the host in order
to make future upgrades work.

This adds basic support for hotfixes, which are RPMs that live
in the extensions container, but are intended to be applied-live
by the MCD.

They appear in `/usr/share/rpm-ostree/extensions/hotfixes` with
the hotfix data (YAML) reserialized to JSON.
  • Loading branch information
cgwalters committed Feb 14, 2023
1 parent 4a6b256 commit 3ad4cf5
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
111 changes: 111 additions & 0 deletions cmd/build-extensions-container.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"fmt"
"os/exec"

cosamodel "github.com/coreos/coreos-assembler/internal/pkg/cosa"
"github.com/coreos/coreos-assembler/internal/pkg/cosash"
cosa "github.com/coreos/coreos-assembler/pkg/builds"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"

"crypto/sha256"
"encoding/json"
Expand All @@ -16,6 +18,108 @@ import (
"time"
)

// hotfix is an element in hotfixes.yaml which is a repo-locked RPM set.
type hotfix struct {
// URL for associated bug
Link string `json:"link"`
// The operating system major version (e.g. 8 or 9)
OsMajor string `json:"osmajor"`
// Repo used to download packages
Repo string `json:"repo"`
// Names of associated packages
Packages []string `json:"packages"`
}

type hotfixData struct {
Hotfixes []hotfix `json:"hotfixes"`
}

// downloadHotfixes basically just accepts as input a declarative JSON file
// format describing hotfixes, which are repo-locked RPM packages we want to download
// but without any dependencies.
func downloadHotfixes(srcdir, configpath, destdir string) error {
contents, err := os.ReadFile(configpath)
if err != nil {
return err
}

var h hotfixData
if err := yaml.Unmarshal(contents, &h); err != nil {
return fmt.Errorf("failed to deserialize hotfixes: %w", err)
}

fmt.Println("Downloading hotfixes")

for _, fix := range h.Hotfixes {
fmt.Printf("Downloading content for hotfix: %s\n", fix.Link)
// Only enable the repos required for download
reposdir := filepath.Join(srcdir, "yumrepos")
argv := []string{"--disablerepo=*", fmt.Sprintf("--enablerepo=%s", fix.Repo), "--setopt=reposdir=" + reposdir, "download"}
argv = append(argv, fix.Packages...)
cmd := exec.Command("dnf", argv...)
cmd.Dir = destdir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to invoke dnf download: %w", err)
}
}

serializedHotfixes, err := json.Marshal(h)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(destdir, "hotfixes.json"), serializedHotfixes, 0o644)
if err != nil {
return err
}

return nil
}

func generateHotfixes() (string, error) {
hotfixesTmpdir, err := os.MkdirTemp("", "")
if err != nil {
return "", err
}
defer os.RemoveAll(hotfixesTmpdir)

variant, err := cosamodel.GetVariant()
if err != nil {
return "", err
}

wd, err := os.Getwd()
if err != nil {
return "", err
}

srcdir := filepath.Join(wd, "src")
p := fmt.Sprintf("%s/config/hotfixes-%s.yaml", srcdir, variant)
if _, err := os.Stat(p); err == nil {
err := downloadHotfixes(srcdir, p, hotfixesTmpdir)
if err != nil {
return "", fmt.Errorf("failed to download hotfixes: %w", err)
}
} else {
fmt.Printf("No %s found\n", p)
}

out := filepath.Join(wd, "tmp/hotfixes.tar")

// Serialize the hotfix RPMs into a tarball which we can pass via a virtio
// device to the qemu process.
cmd := exec.Command("tar", "-c", "-C", hotfixesTmpdir, "-f", out, ".")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return "", err
}

return out, nil
}

func buildExtensionContainer() error {
cosaBuild, buildPath, err := cosa.ReadBuild("builds", "", "")
if err != nil {
Expand All @@ -24,6 +128,11 @@ func buildExtensionContainer() error {
buildID := cosaBuild.BuildID
fmt.Printf("Generating extensions container for build: %s\n", buildID)

hotfixPath, err := generateHotfixes()
if err != nil {
return fmt.Errorf("generating hotfixes failed: %w", err)
}

arch := cosa.BuilderArch()
sh, err := cosash.NewCosaSh()
if err != nil {
Expand All @@ -35,6 +144,8 @@ func buildExtensionContainer() error {
targetname := cosaBuild.Name + "-" + buildID + "-extensions-container" + "." + arch + ".ociarchive"
process := "runvm -chardev \"file,id=ociarchiveout,path=${tmp_builddir}/\"" + targetname +
" -device \"virtserialport,chardev=ociarchiveout,name=ociarchiveout\"" +
" -drive file=" + hotfixPath + ",if=none,id=hotfixes,format=raw,media=disk,read-only=on" +
" -device virtio-blk,serial=hotfixes,drive=hotfixes" +
" -- /usr/lib/coreos-assembler/build-extensions-container.sh " + arch +
" /dev/virtio-ports/ociarchiveout " + buildID
if err := sh.Process(process); err != nil {
Expand Down
31 changes: 31 additions & 0 deletions internal/pkg/cosa/variant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cosa

import (
"encoding/json"
"fmt"
"os"
)

const initConfigPath = "src/config.json"

type configVariant struct {
Variant string `json:"coreos-assembler.config-variant"`
}

// GetVariant finds the configured variant, or "" if unset
func GetVariant() (string, error) {
contents, err := os.ReadFile(initConfigPath)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
return "", nil
}

var variantData configVariant
if err := json.Unmarshal(contents, &variantData); err != nil {
return "", fmt.Errorf("parsing %s: %w", initConfigPath, err)
}

return variantData.Variant, nil
}
3 changes: 3 additions & 0 deletions src/build-extensions-container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ if [[ -f "${workdir}/src/config.json" ]]; then
variant="$(jq --raw-output '."coreos-assembler.config-variant"' "${workdir}/src/config.json")"
fi

mkdir "${ctx_dir}/hotfixes"
tar -xC "${ctx_dir}/hotfixes" -f /dev/disk/by-id/virtio-hotfixes

# Build the image, replacing the FROM directive with the local image we have.
# The `variant` variable is explicitely unquoted to be skipped when empty.
img=localhost/extensions-container
Expand Down

0 comments on commit 3ad4cf5

Please sign in to comment.