110 lines
3.5 KiB
Go
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)
|
|
}
|