Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ The following parameters are supported:
| `boot_time` | string | Optional. Maximum wait time for instance to boot up. During that time plugin check Cloud-Init signatures. |
| `use_ignition` | string | Enable Fedora CoreOS / Flatcar Linux Ignition support |
| `server_spec` | object | Server spec used to create instances. See: [Compute API](https://docs.openstack.org/api-ref/compute/#create-server) |
| `volume_type` | string | Optional. Volume type name, must be used in conjunction with `volume_size` |
| `volume_size` | int | Optional. Size of disk volume in gigabytes, must be used in conjunction with `volume_type` |


### Default connector config
Expand Down
26 changes: 24 additions & 2 deletions internal/openstackclient/openstackclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gophercloud/gophercloud/v2"
"github.com/gophercloud/gophercloud/v2/openstack"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v2/volumes"
"github.com/gophercloud/gophercloud/v2/openstack/config"
"github.com/gophercloud/gophercloud/v2/openstack/config/clouds"
"github.com/gophercloud/gophercloud/v2/openstack/image/v2/images"
Expand Down Expand Up @@ -81,11 +82,15 @@ type Client interface {
ListServers(ctx context.Context) ([]servers.Server, error)
CreateServer(ctx context.Context, spec servers.CreateOptsBuilder, hintOpts servers.SchedulerHintOptsBuilder) (*servers.Server, error)
DeleteServer(ctx context.Context, serverId string) error
CreateVolume(ctx context.Context, opts volumes.CreateOpts) (*volumes.Volume, error)
GetVolume(ctx context.Context, volumeId string) (*volumes.Volume, error)
WaitForVolumeStatus(ctx context.Context, volumeId string, status string) error
}

type client struct {
compute *gophercloud.ServiceClient
image *gophercloud.ServiceClient
compute *gophercloud.ServiceClient
image *gophercloud.ServiceClient
blockStorage *gophercloud.ServiceClient
}

func New(ctx context.Context, authConfig AuthConfig, cloudOpts *CloudOpts) (Client, error) {
Expand Down Expand Up @@ -118,10 +123,15 @@ func New(ctx context.Context, authConfig AuthConfig, cloudOpts *CloudOpts) (Clie
if err != nil {
return nil, err
}
blockStorageClient, err := openstack.NewBlockStorageV3(providerClient, endpointOps)
if err != nil {
return nil, err
}

return &client{
compute: computeClient,
image: imageClient,
blockStorage: blockStorageClient,
}, nil
}

Expand Down Expand Up @@ -315,3 +325,15 @@ func (c *client) CreateServer(ctx context.Context, spec servers.CreateOptsBuilde
func (c *client) DeleteServer(ctx context.Context, serverId string) error {
return servers.Delete(ctx, c.compute, serverId).ExtractErr()
}

func (c *client) CreateVolume(ctx context.Context, opts volumes.CreateOpts) (*volumes.Volume, error) {
return volumes.Create(ctx, c.blockStorage, opts, nil).Extract()
}

func (c *client) GetVolume(ctx context.Context, volumeId string) (*volumes.Volume, error) {
return volumes.Get(ctx, c.blockStorage, volumeId).Extract()
}

func (c *client) WaitForVolumeStatus(ctx context.Context, volumeId string, status string) error {
return volumes.WaitForStatus(ctx, c.blockStorage, volumeId, status)
}
31 changes: 31 additions & 0 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync/atomic"
"time"

"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v2/volumes"
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
"github.com/hashicorp/go-hclog"
"github.com/jinzhu/copier"
Expand All @@ -29,6 +30,8 @@ type InstanceGroup struct {
UseIgnition bool `json:"use_ignition"` // Configure keys via Ignition (Fedora CoreOS / Flatcar)
BootTimeS string `json:"boot_time"` // optional: wait some time before report machine as available
BootTime time.Duration
VolumeType string `json:"volume_type"`
VolumeSize int `json:"volume_size"`

client openstackclient.Client
settings provider.Settings
Expand Down Expand Up @@ -242,6 +245,34 @@ func (g *InstanceGroup) createInstance(ctx context.Context) (string, error) {
}
}

if g.VolumeSize != 0 && g.VolumeType != "" {
volumeOpts := volumes.CreateOpts{
Name: spec.Name,
Size: g.VolumeSize,
VolumeType: g.VolumeType,
ImageID: spec.ImageRef,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec.ImageRef is possibly not known at this point and will only be available after this block ending with line 286.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this can be implemented, I also need this feature for our openstack project.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, you don't need to wait for this feature to use volume-backed instances, you can include block_device_mapping_v2 in the server_spec already:
https://gitlab.com/alasca.cloud/infra/configuration/-/blob/86f078fb4dfa5db5da8111fa202abd56c814e5e0/gitlab-runner-managers/grm-yaook-ephemeral.tpl.toml#L54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — we're already using block_device_mapping_v2 in server_spec, similar to your example. The blocker for us isn't enabling volume-backed boot; it's image rotation without redeploying the runner managers.

Our setup:

Packer builds a new worker image every two weeks under a stable name (ubuntu-2404-glrunner), archiving the previous one so only one active image keeps that name.
Terraform resolves the image UUID at controller deploy time and bakes it into /etc/gitlab-runner/config.toml in block_device_mapping_v2.uuid.
We also set image_name in server_spec, which the fleeting plugin resolves to imageRef on each worker create.
The problem is that with destination_type = "volume", Nova boots from the BDM uuid, not from imageRef. The plugin's image_name resolution only updates imageRef — it does not update block_device_mapping_v2[].uuid. So after a packer run, Glance has a new image UUID under the same name, but controllers still have the old UUID hardcoded in BDM, and new workers keep booting from the old image.

The only workaround that works today without a plugin change would be redeploying controllers after each packer build — but that's not something we'd want to adopt as a process. It would only work because Terraform re-runs the image lookup at deploy time and re-bakes the new UUID into config.toml. It's operational overhead on a two-week cadence, not a real solution to the problem.

What we actually need is for the plugin to resolve image_name into BDM UUIDs at worker create time (same as it already does for imageRef), so managers don't need to be redeployed just to pick up a new image.

}
volume, err := g.client.CreateVolume(ctx, volumeOpts)
if err != nil {
return "", err
}

err = g.client.WaitForVolumeStatus(ctx, volume.ID, "available")
if err != nil {
return "", err
}

Comment on lines +248 to +264

@ProbstDJakob ProbstDJakob Nov 25, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this part as well as reverting all changes in the internal/openstackclient/openstackclient.go in favour of the block_device_mapping_v2.source_type: "image". Thus changing the following part to:

servers.BlockDevice{
	BootIndex:           0,
	DeleteOnTermination: true,
	DestinationType:     servers.DestinationVolume,
	SourceType:          servers.SourceImage,
	UUID:                spec.ImageRef,
	VolumeSize:          g.VolumeSize,
	VolumeType:          g.VolumeType,
}

spec.BlockDevice = []servers.BlockDevice{
{
BootIndex: 0,
DeleteOnTermination: true,
DestinationType: servers.DestinationVolume,
SourceType: servers.SourceVolume,
UUID: volume.ID,
},
}
}
Comment on lines +265 to +274

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think blindly overwriting the spec.BlockDevices is a good idea. I would append the block device and check within the InstanceGroup.Init if the g.ServerSpec.BlockDevices already contain a block device with a boot index set to 0 (in case that VolumeType and VolumeSize are not empty) and return an error.


if spec.ImageName != "" {
imageRef, imgProps, err := g.client.GetImageByName(ctx, spec.ImageName)
if err != nil {
Expand Down