debian-forge-cli/cmd/image-builder/upload.go
Michael Vogt 6169260dcd image-builder: update upload API to match latest images
This commit updates the upload API to match the latest
version in the images library.
2025-08-07 10:29:39 +00:00

184 lines
5 KiB
Go

package main
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/cheggaaa/pb/v3"
"github.com/spf13/cobra"
"github.com/osbuild/image-builder-cli/pkg/progress"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/cloud"
"github.com/osbuild/images/pkg/cloud/awscloud"
"github.com/osbuild/images/pkg/platform"
)
// ErrMissingUploadConfig is returned when the upload configuration is missing
var ErrMissingUploadConfig = errors.New("missing upload configuration")
// ErrUploadConfigNotProvided is returned when all the upload configuration is missing
var ErrUploadConfigNotProvided = errors.New("missing all upload configuration")
// ErrUploadTypeUnsupported is returned when the upload type is not supported
var ErrUploadTypeUnsupported = errors.New("unsupported type")
var awscloudNewUploader = awscloud.NewUploader
func uploadImageWithProgress(uploader cloud.Uploader, imagePath string) error {
f, err := os.Open(imagePath)
if err != nil {
return err
}
defer f.Close()
// setup basic progress
st, err := f.Stat()
if err != nil {
return fmt.Errorf("cannot stat upload: %v", err)
}
pbar := pb.New64(st.Size())
pbar.Set(pb.Bytes, true)
pbar.SetWriter(osStdout)
r := pbar.NewProxyReader(f)
pbar.Start()
defer pbar.Finish()
return uploader.UploadAndRegister(r, osStderr)
}
func uploaderCheckWithProgress(pbar progress.ProgressBar, uploader cloud.Uploader) error {
pr, pw := io.Pipe()
defer pw.Close()
go func() {
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
pbar.SetMessagef("%s", scanner.Text())
}
}()
return uploader.Check(pw)
}
func uploaderFor(cmd *cobra.Command, typeOrCloud string, targetArch string, bootMode *platform.BootMode) (cloud.Uploader, error) {
switch typeOrCloud {
case "ami", "aws":
return uploaderForCmdAWS(cmd, targetArch, bootMode)
default:
return nil, fmt.Errorf("%w: %q", ErrUploadTypeUnsupported, typeOrCloud)
}
}
func uploaderForCmdAWS(cmd *cobra.Command, targetArchStr string, bootMode *platform.BootMode) (cloud.Uploader, error) {
amiName, err := cmd.Flags().GetString("aws-ami-name")
if err != nil {
return nil, err
}
bucketName, err := cmd.Flags().GetString("aws-bucket")
if err != nil {
return nil, err
}
region, err := cmd.Flags().GetString("aws-region")
if err != nil {
return nil, err
}
if bootMode == nil {
// If unset, default to BOOT_HYBIRD which translated
// to "uefi-prefered" when registering the image.
// This should give us wide compatibility. Ideally
// we would introspect the image but we have no
// metadata there right now.
// XXX: move this into the "images" library itself?
bootModeHybrid := platform.BOOT_HYBRID
bootMode = &bootModeHybrid
}
targetArch, err := arch.FromString(targetArchStr)
if err != nil {
return nil, err
}
var missing []string
requiredArgs := []string{"aws-ami-name", "aws-bucket", "aws-region"}
for _, argName := range requiredArgs {
arg, err := cmd.Flags().GetString(argName)
if err != nil {
return nil, err
}
if arg == "" {
missing = append(missing, fmt.Sprintf("--%s", argName))
}
}
if len(missing) > 0 {
if len(missing) == len(requiredArgs) {
return nil, fmt.Errorf("%w: %q", ErrUploadConfigNotProvided, missing)
}
return nil, fmt.Errorf("%w: %q", ErrMissingUploadConfig, missing)
}
opts := &awscloud.UploaderOptions{
TargetArch: targetArch,
BootMode: bootMode,
}
return awscloudNewUploader(region, bucketName, amiName, opts)
}
func detectArchFromImagePath(imagePath string) string {
// This detection is currently rather naive, we just look for
// the file name and try to infer from that. We could extend
// this to smartz like inspect the image via libguestfs or
// add extra metadata to the image. But for now this is what
// we got.
// imagePath by default looks like
// /path/to/<disro>-<imgtype>-<arch>.img.xz
// so try to infer the arch
baseName := filepath.Base(imagePath)
nameNoEx := strings.SplitN(baseName, ".", -1)[0]
frags := strings.Split(nameNoEx, "-")
maybeArch := frags[len(frags)-1]
if a, err := arch.FromString(maybeArch); err == nil {
return a.String()
}
return ""
}
func cmdUpload(cmd *cobra.Command, args []string) error {
imagePath := args[0]
uploadTo, err := cmd.Flags().GetString("to")
if err != nil {
return err
}
if uploadTo == "" {
return fmt.Errorf("missing --to parameter, try --to=aws")
}
targetArch, err := cmd.Flags().GetString("arch")
if err != nil {
return err
}
if targetArch == "" {
targetArch = detectArchFromImagePath(imagePath)
if targetArch != "" {
fmt.Fprintf(osStderr, "Note: using architecture %q based on image filename (use --arch to override)\n", targetArch)
}
if targetArch == "" {
targetArch = arch.Current().String()
fmt.Fprintf(osStderr, "WARNING: no upload architecture specified, using %q (use --arch to override)\n", targetArch)
}
}
uploader, err := uploaderFor(cmd, uploadTo, targetArch, nil)
if err != nil {
return err
}
return uploadImageWithProgress(uploader, imagePath)
}