Files

110 lines
3.5 KiB
Go

package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"
"felis.lolicon.best/internal/imagepush"
)
// cmdImageBundle writes a release's image bundle: one OCI layout tar holding every
// image an install runs, for one platform, plus its listing (one "role name
// manifest-digest config-digest" line per image). deploy/build-release-artifacts.sh
// runs it in CI; deploy/bootstrap.sh imports the tar into k3s's containerd and
// pushes it into the platform registry with push-image --image.
//
// --layout role=name=path an image buildx wrote with --output type=oci
// --pull role=ref a digest-pinned public image, named repository@digest
//
// The tar and the listing are written beside their final paths and renamed into
// place, so a failed run leaves neither behind.
func cmdImageBundle(args []string, _, stderr io.Writer) int {
fs := flag.NewFlagSet("image-bundle", flag.ContinueOnError)
fs.SetOutput(stderr)
platform := fs.String("platform", "", "os/arch the bundle is for, e.g. linux/arm64")
out := fs.String("out", "", "path of the bundle tar to write")
list := fs.String("list", "", "path of the listing to write")
var images []imagepush.BundleImage
fs.Func("layout", "role=name=path of an OCI layout tar (repeatable)", func(v string) error {
role, rest, ok := strings.Cut(v, "=")
name, path, ok2 := strings.Cut(rest, "=")
if !ok || !ok2 || role == "" || name == "" || path == "" {
return fmt.Errorf("want role=name=path, got %q", v)
}
images = append(images, imagepush.BundleImage{Role: role, Name: name, Layout: path})
return nil
})
fs.Func("pull", "role=ref of a digest-pinned public image (repeatable)", func(v string) error {
role, ref, ok := strings.Cut(v, "=")
if !ok || role == "" || !strings.Contains(ref, "@sha256:") {
return fmt.Errorf("want role=ref with ref pinned by digest, got %q", v)
}
images = append(images, imagepush.BundleImage{Role: role, Name: imagepush.PinnedName(ref), Source: ref})
return nil
})
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if *platform == "" || *out == "" || *list == "" || len(images) == 0 {
fmt.Fprintln(stderr, "felis image-bundle: --platform, --out, --list and at least one --layout or --pull are required")
return 2
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := writeImageBundle(ctx, &imagepush.Source{Platform: *platform}, images, *out, *list); err != nil {
fmt.Fprintf(stderr, "felis image-bundle: %v\n", err)
return 1
}
return 0
}
func writeImageBundle(ctx context.Context, s *imagepush.Source, images []imagepush.BundleImage, out, list string) (err error) {
tmpOut, tmpList := out+".tmp", list+".tmp"
defer func() {
if err != nil {
os.Remove(tmpOut)
os.Remove(tmpList)
}
}()
f, err := os.Create(tmpOut)
if err != nil {
return err
}
w := bufio.NewWriterSize(f, 1<<20)
entries, err := imagepush.WriteBundle(ctx, s, images, w)
if err == nil {
err = w.Flush()
}
if err == nil {
err = f.Sync()
}
if cerr := f.Close(); err == nil {
err = cerr
}
if err != nil {
return err
}
var b strings.Builder
for _, e := range entries {
fmt.Fprintf(&b, "%s %s %s %s\n", e.Role, e.Name, e.Digest, e.Config)
}
if err := os.WriteFile(tmpList, []byte(b.String()), 0o644); err != nil {
return err
}
if err := os.Rename(tmpOut, out); err != nil {
return err
}
return os.Rename(tmpList, list)
}