main: add new upload command

This commit adds a new `upload` command that can be used to
upload a raw image to the cloud. Currently only AWS is
supported but as images adds more clouds to the uploader interfac
we can easily expand more.

The cloud is currently detected via the file extension, that
mapping probably should also go into the `images` library.
This commit is contained in:
Michael Vogt 2025-01-29 17:46:53 +01:00
parent 0678d8ddfd
commit e41377b82a
5 changed files with 162 additions and 2 deletions

View file

@ -5,6 +5,8 @@ import (
"io"
"os"
"github.com/osbuild/images/pkg/cloud"
"github.com/osbuild/images/pkg/cloud/awscloud"
"github.com/osbuild/images/pkg/reporegistry"
)
@ -60,3 +62,11 @@ func MockDistroGetHostDistroName(f func() (string, error)) (restore func()) {
distroGetHostDistroName = saved
}
}
func MockAwscloudNewUploader(f func(string, string, string, *awscloud.UploaderOptions) (cloud.Uploader, error)) (restore func()) {
saved := awscloudNewUploader
awscloudNewUploader = f
return func() {
awscloudNewUploader = saved
}
}

View file

@ -6,13 +6,16 @@ import (
"io"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/cheggaaa/pb/v3"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/osbuild/bootc-image-builder/bib/pkg/progress"
"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/cloud/awscloud"
"github.com/osbuild/images/pkg/imagefilter"
"github.com/osbuild/images/pkg/osbuild"
"github.com/osbuild/images/pkg/ostree"
@ -173,6 +176,66 @@ func progressFromCmd(cmd *cobra.Command) (progress.ProgressBar, error) {
return progress.New(progressType)
}
var awscloudNewUploader = awscloud.NewUploader
func cmdUploadAWS(cmd *cobra.Command, args []string) error {
amiName, err := cmd.Flags().GetString("aws-ami-name")
if err != nil {
return err
}
bucketName, err := cmd.Flags().GetString("aws-bucket")
if err != nil {
return err
}
region, err := cmd.Flags().GetString("aws-region")
if err != nil {
return err
}
rawDiskPath := args[0]
// XXX: can we actually inspect the image or leave some artifacts?
if filepath.Ext(rawDiskPath) != ".raw" {
return fmt.Errorf("expecting a raw disk ending with '.raw', got %q", filepath.Base(rawDiskPath))
}
uploader, err := awscloudNewUploader(region, bucketName, amiName, nil)
if err != nil {
return err
}
f, err := os.Open(rawDiskPath)
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 cmdUpload(cmd *cobra.Command, args []string) error {
uploadTo, err := cmd.Flags().GetString("to")
if err != nil {
return err
}
switch uploadTo {
case "aws":
return cmdUploadAWS(cmd, args)
default:
return fmt.Errorf("unsupported cloud %q", uploadTo)
}
}
func cmdBuild(cmd *cobra.Command, args []string) error {
cacheDir, err := cmd.Flags().GetString("cache")
if err != nil {
@ -300,6 +363,19 @@ operating systems like Fedora, CentOS and RHEL with easy customizations support.
manifestCmd.Flags().Bool("with-sbom", false, `export SPDX SBOM document`)
rootCmd.AddCommand(manifestCmd)
uploadCmd := &cobra.Command{
Use: "upload <image-path>",
Short: "Upload the given image from <image-path>",
RunE: cmdUpload,
SilenceUsage: true,
Args: cobra.ExactArgs(1),
}
uploadCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)")
uploadCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)")
uploadCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)")
rootCmd.AddCommand(uploadCmd)
uploadCmd.Flags().String("to", "", "upload to the given cloud")
buildCmd := &cobra.Command{
Use: "build <image-type>",
Short: "Build the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)",

View file

@ -15,8 +15,10 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
//"github.com/osbuild/bootc-image-builder/bib/pkg/progress"
"github.com/osbuild/images/pkg/cloud"
"github.com/osbuild/images/pkg/cloud/awscloud"
testrepos "github.com/osbuild/images/test/data/repositories"
main "github.com/osbuild/image-builder-cli/cmd/image-builder"
@ -600,3 +602,64 @@ func TestProgressFromCmd(t *testing.T) {
assert.Equal(t, tc.expectedProgress, fmt.Sprintf("%T", pbar))
}
}
type fakeAwsUploader struct {
checkCalls int
uploadAndRegisterRead bytes.Buffer
uploadAndRegisterCalls int
}
var _ = cloud.Uploader(&fakeAwsUploader{})
func (fa *fakeAwsUploader) Check(status io.Writer) error {
fa.checkCalls++
return nil
}
func (fa *fakeAwsUploader) UploadAndRegister(r io.Reader, status io.Writer) error {
fa.uploadAndRegisterCalls++
_, err := io.Copy(&fa.uploadAndRegisterRead, r)
return err
}
func TestUploadWithAWSMock(t *testing.T) {
fakeDiskContent := "fake-raw-img"
fakeImageFilePath := filepath.Join(t.TempDir(), "disk.raw")
err := os.WriteFile(fakeImageFilePath, []byte(fakeDiskContent), 0644)
assert.NoError(t, err)
var regionName, bucketName, amiName string
var fa fakeAwsUploader
restore := main.MockAwscloudNewUploader(func(region string, bucket string, ami string, opts *awscloud.UploaderOptions) (cloud.Uploader, error) {
regionName = region
bucketName = bucket
amiName = ami
return &fa, nil
})
defer restore()
var fakeStdout bytes.Buffer
restore = main.MockOsStdout(&fakeStdout)
defer restore()
restore = main.MockOsArgs([]string{
"upload",
"--to=aws",
"--aws-region=aws-region-1",
"--aws-bucket=aws-bucket-2",
"--aws-ami-name=aws-ami-3",
fakeImageFilePath,
})
err = main.Run()
require.NoError(t, err)
assert.Equal(t, regionName, "aws-region-1")
assert.Equal(t, bucketName, "aws-bucket-2")
assert.Equal(t, amiName, "aws-ami-3")
assert.Equal(t, fakeDiskContent, fa.uploadAndRegisterRead.String())
// progress was rendered
assert.Contains(t, fakeStdout.String(), "--] 100.00%")
}