From d4c95dfd1b3237376cd716c6ea20502ae4284d83 Mon Sep 17 00:00:00 2001 From: Jakub Kadlcik Date: Wed, 1 Oct 2025 11:01:14 +0200 Subject: [PATCH] Support uploading to OpenStack In Copr, we upload our builder images to OSUOSL OpenStack using the `glance image-create` command. I think it would be really cool if we could use `image-builder upload` instead. --- go.mod | 1 + go.sum | 2 + pkg/cloud/openstack/openstack.go | 93 ++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 pkg/cloud/openstack/openstack.go diff --git a/go.mod b/go.mod index ab6034964b..c6335a9add 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/gobwas/glob v0.2.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 + github.com/gophercloud/gophercloud/v2 v2.8.0 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-version v1.7.0 github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b diff --git a/go.sum b/go.sum index bee83eca80..4499fecb58 100644 --- a/go.sum +++ b/go.sum @@ -252,6 +252,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo= +github.com/gophercloud/gophercloud/v2 v2.8.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= diff --git a/pkg/cloud/openstack/openstack.go b/pkg/cloud/openstack/openstack.go new file mode 100644 index 0000000000..7d521050ce --- /dev/null +++ b/pkg/cloud/openstack/openstack.go @@ -0,0 +1,93 @@ +package openstack + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/osbuild/images/pkg/cloud" + + "github.com/gophercloud/gophercloud/v2" + ostack "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" +) + +var _ = cloud.Uploader(&openstackUploader{}) + +type openstackUploader struct { + image string + diskFormat string + containerFormat string +} + +type UploaderOptions struct { + DiskFormat string + ContainerFormat string +} + +func NewUploader(image string, opts *UploaderOptions) (cloud.Uploader, error) { + return &openstackUploader{ + image: image, + diskFormat: opts.DiskFormat, + containerFormat: opts.ContainerFormat, + }, nil +} + +func (ou *openstackUploader) Check(status io.Writer) error { + return nil +} + +func (ou *openstackUploader) UploadAndRegister(r io.Reader, uploadSize uint64, status io.Writer) (err error) { + fmt.Fprintf(status, "Uploading to OpenStack...\n") + + opts, err := ostack.AuthOptionsFromEnv() + if err != nil { + return fmt.Errorf("Failed to read OpenStack ENV variables. Please source the OpenStack RC file: %w", err) + } + + // This is needed otherwise we get the following error when authenticating: + // You must provide exactly one of DomainID or DomainName to + // authenticate by Username + // Even with an RC file that works perfectly fine with `openstack token issue` + // See https://github.com/gophercloud/gophercloud/issues/3440 + // See https://github.com/gophercloud/gophercloud/issues/3240 + if opts.DomainName == "" { + opts.DomainName = os.Getenv("OS_USER_DOMAIN_NAME") + } + + ctx := context.Background() + provider, err := ostack.AuthenticatedClient(ctx, opts) + if err != nil { + return fmt.Errorf("Failed to authenticate to OpenStack: %w", err) + } + + client, err := ostack.NewImageV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + if err != nil { + return fmt.Errorf("Failed to initialize the client: %w", err) + } + + createOpts := images.CreateOpts{ + Name: ou.image, + DiskFormat: ou.diskFormat, + ContainerFormat: ou.containerFormat, + } + img, err := images.Create(ctx, client, createOpts).Extract() + if err != nil { + return fmt.Errorf("Failed to create the image metadata: %w", err) + } + + err = imagedata.Upload(ctx, client, img.ID, r).ExtractErr() + if err != nil { + return fmt.Errorf("Failed to upload the image: %w", err) + } + + // This would glitch the progressbar, but once it gets fixed, we would + // like to print this message + // fmt.Printf("Created image: %s (ID: %s)\n", img.Name, img.ID) + + return nil +}