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/--.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) }