From b20913406b883b2f7477ef0dc4adef4cd4745167 Mon Sep 17 00:00:00 2001 From: Friedrich Zahn Date: Mon, 8 Sep 2025 10:36:54 +0200 Subject: [PATCH] Add option to specify the instance flavor by name This adds a `flavor_name` config option, that takes precedence over `flavorRef` and is used to look up the Flavor by that name. --- README.md | 1 + internal/openstackclient/openstackclient.go | 25 +++++++++++ .../openstackclient/openstackclient_test.go | 41 +++++++++++++++++++ provider.go | 11 +++++ testdata/flavor_list_none.json | 3 ++ testdata/flavor_list_one.json | 26 ++++++++++++ utils.go | 3 ++ 7 files changed, 110 insertions(+) create mode 100644 testdata/flavor_list_none.json create mode 100644 testdata/flavor_list_one.json diff --git a/README.md b/README.md index 0add6a9..2cd522d 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ tags = ["GitLab", "CI", "Docker", "Scaling"] imageRef = "d5460af5-83f3-47d7-9c4f-80294c66b267" # Flatcar Linux (ID) image_name = "flatcar" # Resolve imageRef. If set, each time a new VM should be created, the imageRef will be resolved. flavorRef = "4e9d4fa4-a703-4850-8bc1-58b5e139ab57" # xlarge flavor +flavor_name = "XL" # Resolve flavorRef. If set, each time a new VM should be created, the flavorRef will be resolved. # key_name = "ci-admin" # SSH public key for worker nodes networks = [ { uuid = "f05e7f64-9e0f-4c5c-acb0-b636000d7301" } ] # tenant network security_groups = [ "cee22d91-bb9a-455d-be88-e911d3cb066a" ] # allow SSH ingress from tenant network diff --git a/internal/openstackclient/openstackclient.go b/internal/openstackclient/openstackclient.go index 5b7e53d..c952cef 100644 --- a/internal/openstackclient/openstackclient.go +++ b/internal/openstackclient/openstackclient.go @@ -10,6 +10,7 @@ import ( "github.com/go-viper/mapstructure/v2" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/v2/openstack/config" "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" @@ -76,6 +77,7 @@ type ImageProperties struct { type Client interface { GetImageProperties(ctx context.Context, imageRef string) (*ImageProperties, error) GetImageByName(ctx context.Context, imageName string) (string, *ImageProperties, error) + GetFlavorByName(ctx context.Context, flavorName string) (string, error) ShowServerConsoleOutput(ctx context.Context, serverId string) (string, error) GetServer(ctx context.Context, serverId string) (*servers.Server, error) ListServers(ctx context.Context) ([]servers.Server, error) @@ -284,6 +286,29 @@ func (c *client) GetImageByName(ctx context.Context, imageName string) (string, return imgs[0].ID, out, nil } +func (c *client) GetFlavorByName(ctx context.Context, flavorName string) (string, error) { + // Nova API doesn't support filtering by Name, so we have to do that on our end + page, err := flavors.ListDetail(c.compute, nil).AllPages(ctx) + if err != nil { + return "", fmt.Errorf("failed to list flavors: %w", err) + } + + flavs, err := flavors.ExtractFlavors(page) + if err != nil { + return "", fmt.Errorf("failed to parse flavors: %w", err) + } + + for _, flav := range flavs { + if flav.Name == flavorName { + // Flavor Names are unique per Nova db schema, so we can immediately return on a match + return flav.ID, nil + } + } + + err = gophercloud.ErrResourceNotFound{Name: flavorName, ResourceType: "flavor"} + return "", err +} + func (c *client) ShowServerConsoleOutput(ctx context.Context, serverId string) (string, error) { return servers.ShowConsoleOutput(ctx, c.compute, serverId, servers.ShowConsoleOutputOpts{ Length: 100, diff --git a/internal/openstackclient/openstackclient_test.go b/internal/openstackclient/openstackclient_test.go index ceb960f..0a13798 100644 --- a/internal/openstackclient/openstackclient_test.go +++ b/internal/openstackclient/openstackclient_test.go @@ -85,3 +85,44 @@ func TestGetImageByName_Many(t *testing.T) { _, _, err = client.GetImageByName(ctx, "flatcar") assert.ErrorIs(err, gophercloud.ErrMultipleResourcesFound{Name: "flatcar", Count: 8, ResourceType: "image"}) } + +func TestGetFlavorByName(t *testing.T) { + assert := assert.New(t) + + flav, err := os.ReadFile("../../testdata/flavor_list_one.json") + require.NoError(t, err) + + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.ServeFile(t, "", "", "application/json", string(flav)) + + client := &client{ + compute: thclient.ServiceClient(), + } + + ctx := context.TODO() + flavorRef, err := client.GetFlavorByName(ctx, "S") + assert.NoError(err) + assert.Equal("113", flavorRef) +} + +func TestGetFlavorByName_None(t *testing.T) { + assert := assert.New(t) + + flav, err := os.ReadFile("../../testdata/flavor_list_none.json") + require.NoError(t, err) + + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.ServeFile(t, "", "", "application/json", string(flav)) + + client := &client{ + compute: thclient.ServiceClient(), + } + + ctx := context.TODO() + _, err = client.GetFlavorByName(ctx, "S") + assert.ErrorIs(err, gophercloud.ErrResourceNotFound{Name: "S", ResourceType: "flavor"}) +} diff --git a/provider.go b/provider.go index 532b23b..fc33e41 100644 --- a/provider.go +++ b/provider.go @@ -254,6 +254,17 @@ func (g *InstanceGroup) createInstance(ctx context.Context) (string, error) { g.log.Debug("Image resolved by name", "image_name", spec.ImageName, "image_ref", spec.ImageRef) } + if spec.FlavorName != "" { + flavorRef, err := g.client.GetFlavorByName(ctx, spec.FlavorName) + if err != nil { + return "", err + } + + spec.FlavorRef = flavorRef + + g.log.Debug("Flavor resolved by name", "flavor_name", spec.FlavorName, "image_ref", spec.FlavorRef) + } + srv, err := g.client.CreateServer(ctx, spec, hintOpts) if err != nil { return "", err diff --git a/testdata/flavor_list_none.json b/testdata/flavor_list_none.json new file mode 100644 index 0000000..d4e8e60 --- /dev/null +++ b/testdata/flavor_list_none.json @@ -0,0 +1,3 @@ +{ + "flavors": [] +} diff --git a/testdata/flavor_list_one.json b/testdata/flavor_list_one.json new file mode 100644 index 0000000..0675fd9 --- /dev/null +++ b/testdata/flavor_list_one.json @@ -0,0 +1,26 @@ +{ + "flavors": [ + { + "id": "113", + "name": "S", + "ram": 2048, + "disk": 15, + "swap": "", + "OS-FLV-EXT-DATA:ephemeral": 0, + "OS-FLV-DISABLED:disabled": false, + "vcpus": 1, + "os-flavor-access:is_public": true, + "rxtx_factor": 1.0, + "links": [ + { + "rel": "self", + "href": "https://nova.example.org:8774/v2.1/743713f9aba64e4e927b68cb233d8d27/flavors/113" + }, + { + "rel": "bookmark", + "href": "https://nova.example.org:8774/743713f9aba64e4e927b68cb233d8d27/flavors/113" + } + ] + } + ] +} diff --git a/utils.go b/utils.go index 5785115..0605943 100644 --- a/utils.go +++ b/utils.go @@ -26,6 +26,9 @@ type ExtCreateOpts struct { // search for imageRef by name each time ImageName string `json:"image_name,omitempty"` + // search for flavorRef by name each time + FlavorName string `json:"flavor_name,omitempty"` + // annotation overrides Networks []servers.Network `json:"networks,omitempty"` SecurityGroups []string `json:"security_groups,omitempty"`