From 5b786a8879995e7f31f6aa23bdcd1732bf1e8489 Mon Sep 17 00:00:00 2001 From: Matthias Baur Date: Fri, 6 Dec 2024 13:07:59 +0100 Subject: [PATCH 1/6] Allow authentication through environment variables This allows the usage of standardized OS_* variables to authenticate against OpenStack. It's helpful when running the plugin in Kubernetes, because you "only" need to inject environment variables into the pod, not a full YAML file. --- README.md | 3 +++ provider.go | 74 ++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index da5d015..8e33405 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The following parameters are supported: |-----------------------|--------|-------------| | `cloud` | string | Name of the cloud config from clouds.yaml to use | | `clouds_config` | string | Optional. Path to clouds.yaml | +| `auth_from_env` | bool | Optional. Use environment variables for authentication | | `name` | string | Name of the Auto Scaling Group (unique string that used to find instances) | | `nova_microversion` | string | Optional. Microversion for the Openstack Nova client. Default 2.79 (which should be ok for Train+) | | `boot_time` | string | Optional. Maximum wait time for instance to boot up. During that time plugin check Cloud-Init signatures. | @@ -38,6 +39,8 @@ OpenStack setup 1. You should create a special user (recommended) and project (optional), then export clouds.yaml with credentials for that cloud. + 1. Optional: You can also use OS\_\* environment variables to authenticate. + 2. You may create a tenant network for workers, in that case don't forget to add a router. In that case manager VM should have two ports: external and that tenant network, so it will be able to connect to the worker instances. diff --git a/provider.go b/provider.go index ca9e212..a2e9536 100644 --- a/provider.go +++ b/provider.go @@ -3,8 +3,10 @@ package fpoc import ( "bytes" "context" + "crypto/tls" "errors" "fmt" + "os" "path" "sync/atomic" "time" @@ -29,6 +31,7 @@ var _ provider.InstanceGroup = (*InstanceGroup)(nil) type InstanceGroup struct { Cloud string `json:"cloud"` // cloud to use CloudsConfig string `json:"clouds_config"` // optional: path to clouds.yaml + AuthFromEnv bool `json:"auth_from_env"` // optional: Use environment variables for authentication Name string `json:"name"` // name of the cluster NovaMicroversion string `json:"nova_microversion"` // Microversion for the Nova client ServerSpec ExtCreateOpts `json:"server_spec"` // instance creation spec @@ -45,25 +48,15 @@ type InstanceGroup struct { } func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings provider.Settings) (provider.ProviderInfo, error) { - pOpts := []clouds.ParseOption{clouds.WithCloudName(g.Cloud)} - if g.CloudsConfig != "" { - pOpts = append(pOpts, clouds.WithLocations(g.CloudsConfig)) - } - - ao, eo, tlsCfg, err := clouds.Parse(pOpts...) - if err != nil { - return provider.ProviderInfo{}, fmt.Errorf("Failed to parse clouds.yaml: %w", err) - } - - // plugin is a long running process. force allow reauth - ao.AllowReauth = true + g.log = log.With("name", g.Name, "cloud", g.Cloud) + g.log.Debug("Initializing fleeting-plugin-openstack") - pc, err := config.NewProviderClient(ctx, ao, config.WithTLSConfig(tlsCfg)) + providerClient, endpointOps, err := g.getProviderClient(ctx) if err != nil { - return provider.ProviderInfo{}, fmt.Errorf("Failed to connect to OpenStack Keystone: %w", err) + return provider.ProviderInfo{}, err } - cli, err := openstack.NewComputeV2(pc, eo) + cli, err := openstack.NewComputeV2(providerClient, endpointOps) if err != nil { return provider.ProviderInfo{}, fmt.Errorf("Failed to connect to OpenStack Nova: %w", err) } @@ -85,7 +78,7 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro } if g.ServerSpec.ImageRef != "" { - imgCli, err := openstack.NewImageV2(pc, eo) + imgCli, err := openstack.NewImageV2(providerClient, endpointOps) if err != nil { return provider.ProviderInfo{}, fmt.Errorf("Failed to get OpenStack Glance: %w", err) } @@ -121,8 +114,6 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro } g.settings = settings - g.log = log.With("name", g.Name, "cloud", g.Cloud) - if _, err := g.getInstances(ctx); err != nil { return provider.ProviderInfo{}, err } @@ -135,6 +126,53 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro }, nil } +func (g *InstanceGroup) getProviderClient(ctx context.Context) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { + var endpointOps gophercloud.EndpointOpts + var authOptions gophercloud.AuthOptions + var providerClient *gophercloud.ProviderClient + + if g.AuthFromEnv { + g.log.Debug("Using env vars for auth") + + var err error + endpointOps = gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} + authOptions, err = openstack.AuthOptionsFromEnv() + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to get auth options from environment: %w", err) + } + authOptions.AllowReauth = true + + providerClient, err = openstack.AuthenticatedClient(ctx, authOptions) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to connect to OpenStack Keystone: %w", err) + } + } else { + g.log.Debug("Using clouds.yaml for auth") + + var err error + var tlsCfg *tls.Config + cloudOpts := []clouds.ParseOption{clouds.WithCloudName(g.Cloud)} + if g.CloudsConfig != "" { + cloudOpts = append(cloudOpts, clouds.WithLocations(g.CloudsConfig)) + } + + authOptions, endpointOps, tlsCfg, err = clouds.Parse(cloudOpts...) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to parse clouds.yaml: %w", err) + } + + // plugin is a long running process. force allow reauth + authOptions.AllowReauth = true + + providerClient, err = config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsCfg)) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to connect to OpenStack Keystone: %w", err) + } + } + + return providerClient, endpointOps, nil +} + func (g *InstanceGroup) Update(ctx context.Context, update func(instance string, state provider.State)) error { instances, err := g.getInstances(ctx) From 01234f4db9c39209f592a96c0ed7a0060e1da6aa Mon Sep 17 00:00:00 2001 From: Matthias Baur Date: Tue, 10 Dec 2024 12:40:33 +0100 Subject: [PATCH 2/6] Refactor to proper openstackclient --- internal/openstackclient/openstackclient.go | 190 ++++++++++++++++++ .../openstackclient/openstackclient_test.go | 38 ++++ provider.go | 120 ++--------- utils.go | 31 --- utils_test.go | 26 --- 5 files changed, 248 insertions(+), 157 deletions(-) create mode 100644 internal/openstackclient/openstackclient.go create mode 100644 internal/openstackclient/openstackclient_test.go diff --git a/internal/openstackclient/openstackclient.go b/internal/openstackclient/openstackclient.go new file mode 100644 index 0000000..247490d --- /dev/null +++ b/internal/openstackclient/openstackclient.go @@ -0,0 +1,190 @@ +package openstackclient + +import ( + "context" + "crypto/tls" + "fmt" + "os" + + "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/config" + "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" + "github.com/gophercloud/gophercloud/v2/openstack/utils" + "github.com/mitchellh/mapstructure" +) + +type AuthConfig struct { + // AuthFromEnv specifies whether to use environment variables for auth + AuthFromEnv bool + + // Cloud is the name of the cloud config from clouds.yaml to use + Cloud string + + // CloudsConfig is the path to the clouds.yaml file + CloudsConfig string + + // NovaMicroversion is the microversion of the OpenStack Nova client. Default 2.79 (which should be ok for Train+) + NovaMicroversion string +} + +// Some good known properties useful for setting up ConnectInfo +// +// See also: https://docs.openstack.org/glance/latest/admin/useful-image-properties.html +type ImageProperties struct { + // Architecture that must be supported by the hypervisor. + Architecture string `json:"architecture,omitempty" mapstructure:"architecture,omitempty"` + + // OSType is the operating system installed on the image. + OSType string `json:"os_type,omitempty" mapstructure:"os_type,omitempty"` + + // OSDistro is the common name of the operating system distribution in lowercase + OSDistro string `json:"os_distro,omitempty" mapstructure:"os_distro,omitempty"` + + // OSVersion is the operating system version as specified by the distributor. + OSVersion string `json:"os_version,omitempty" mapstructure:"os_version,omitempty"` + + // OSAdminUser is the default admin user name for the operating system + OSAdminUser string `json:"os_admin_user,omitempty" mapstructure:"os_admin_user,omitempty"` +} + +type Client interface { + GetImageProperties(ctx context.Context, imageRef string) (*ImageProperties, 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) + CreateServer(ctx context.Context, spec servers.CreateOptsBuilder, hintOpts servers.SchedulerHintOptsBuilder) (*servers.Server, error) + DeleteServer(ctx context.Context, serverId string) error +} + +var _ Client = (*client)(nil) + +type client struct { + compute *gophercloud.ServiceClient + image *gophercloud.ServiceClient +} + +func New(authConfig AuthConfig) (Client, error) { + if authConfig.NovaMicroversion == "" { + authConfig.NovaMicroversion = "2.79" // Train+ + } + + providerClient, endpointOps, err := newProviderClient(authConfig) + if err != nil { + return nil, err + } + + computeClient, err := openstack.NewComputeV2(providerClient, endpointOps) + if err != nil { + return nil, err + } + + _computeClient, err := utils.RequireMicroversion(context.TODO(), *computeClient, authConfig.NovaMicroversion) + if err != nil { + return nil, fmt.Errorf("failed to request microversion %s for OpenStack Nova: %w", authConfig.NovaMicroversion, err) + } + + computeClient = &_computeClient + + imageClient, err := openstack.NewImageV2(providerClient, endpointOps) + if err != nil { + return nil, err + } + + return &client{ + compute: computeClient, + image: imageClient, + }, nil +} + +func newProviderClient(authConfig AuthConfig) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { + var endpointOps gophercloud.EndpointOpts + var authOptions gophercloud.AuthOptions + var providerClient *gophercloud.ProviderClient + + if authConfig.AuthFromEnv { + var err error + endpointOps = gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} + authOptions, err = openstack.AuthOptionsFromEnv() + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to get auth options from environment: %w", err) + } + authOptions.AllowReauth = true + + providerClient, err = openstack.AuthenticatedClient(context.Background(), authOptions) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) + } + } else { + var err error + var tlsCfg *tls.Config + cloudOpts := []clouds.ParseOption{clouds.WithCloudName(authConfig.Cloud)} + if authConfig.CloudsConfig != "" { + cloudOpts = append(cloudOpts, clouds.WithLocations(authConfig.CloudsConfig)) + } + + authOptions, endpointOps, tlsCfg, err = clouds.Parse(cloudOpts...) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } + + // plugin is a long running process. force allow reauth + authOptions.AllowReauth = true + + providerClient, err = config.NewProviderClient(context.TODO(), authOptions, config.WithTLSConfig(tlsCfg)) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) + } + } + + return providerClient, endpointOps, nil +} + +func (c *client) GetImageProperties(ctx context.Context, imageRef string) (*ImageProperties, error) { + image, err := images.Get(ctx, c.image, imageRef).Extract() + if err != nil { + return nil, fmt.Errorf("failed to get image %s: %w", imageRef, err) + } + + out := new(ImageProperties) + err = mapstructure.Decode(image.Properties, out) + if err != nil { + return nil, fmt.Errorf("failed to parse properties: %w", err) + } + + return out, nil +} + +func (c *client) ShowServerConsoleOutput(ctx context.Context, serverId string) (string, error) { + return servers.ShowConsoleOutput(ctx, c.compute, serverId, servers.ShowConsoleOutputOpts{ + Length: 100, + }).Extract() +} + +func (c *client) GetServer(ctx context.Context, serverId string) (*servers.Server, error) { + return servers.Get(ctx, c.compute, serverId).Extract() +} + +func (c *client) ListServers(ctx context.Context) ([]servers.Server, error) { + page, err := servers.List(c.compute, nil).AllPages(ctx) + if err != nil { + return nil, fmt.Errorf("server listing error: %w", err) + } + + allServers, err := servers.ExtractServers(page) + if err != nil { + return nil, fmt.Errorf("server listing extract error: %w", err) + } + + return allServers, nil +} + +func (c *client) CreateServer(ctx context.Context, spec servers.CreateOptsBuilder, hintOpts servers.SchedulerHintOptsBuilder) (*servers.Server, error) { + return servers.Create(ctx, c.compute, spec, hintOpts).Extract() +} + +func (c *client) DeleteServer(ctx context.Context, serverId string) error { + return servers.Delete(ctx, c.compute, serverId).ExtractErr() +} diff --git a/internal/openstackclient/openstackclient_test.go b/internal/openstackclient/openstackclient_test.go new file mode 100644 index 0000000..0c942d0 --- /dev/null +++ b/internal/openstackclient/openstackclient_test.go @@ -0,0 +1,38 @@ +package openstackclient + +import ( + "context" + "os" + "testing" + + "github.com/gophercloud/gophercloud/v2/testhelper" + thclient "github.com/gophercloud/gophercloud/v2/testhelper/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetImageProperties(t *testing.T) { + assert := assert.New(t) + + img, err := os.ReadFile("../../testdata/image_get.json") + require.NoError(t, err) + + testhelper.SetupHTTP() + defer testhelper.TeardownHTTP() + + testhelper.ServeFile(t, "", "", "application/json", string(img)) + + client := &client{ + compute: thclient.ServiceClient(), + image: thclient.ServiceClient(), + } + + ctx := context.TODO() + props, err := client.GetImageProperties(ctx, "1da9661c-953e-424d-a1e5-834a8174b198") + assert.NoError(err) + if assert.NotNil(props) { + assert.Equal("core", props.OSAdminUser) + } + + t.Log(props) +} diff --git a/provider.go b/provider.go index a2e9536..93263a2 100644 --- a/provider.go +++ b/provider.go @@ -3,22 +3,16 @@ package fpoc import ( "bytes" "context" - "crypto/tls" "errors" "fmt" - "os" "path" "sync/atomic" "time" - "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/config" - clouds "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" - osutil "github.com/gophercloud/gophercloud/v2/openstack/utils" "github.com/hashicorp/go-hclog" "github.com/jinzhu/copier" + "github.com/sardinasystems/fleeting-plugin-openstack/internal/openstackclient" "gitlab.com/gitlab-org/fleeting/fleeting/connector" "gitlab.com/gitlab-org/fleeting/fleeting/provider" @@ -28,6 +22,8 @@ const MetadataKey = "fleeting-cluster" var _ provider.InstanceGroup = (*InstanceGroup)(nil) +var newClient = openstackclient.New + type InstanceGroup struct { Cloud string `json:"cloud"` // cloud to use CloudsConfig string `json:"clouds_config"` // optional: path to clouds.yaml @@ -39,10 +35,10 @@ type InstanceGroup struct { BootTimeS string `json:"boot_time"` // optional: wait some time before report machine as available BootTime time.Duration - computeClient *gophercloud.ServiceClient + client openstackclient.Client settings provider.Settings log hclog.Logger - imgProps *ImageProperties + imgProps *openstackclient.ImageProperties sshPubKey string instanceCounter atomic.Int32 } @@ -51,39 +47,25 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro g.log = log.With("name", g.Name, "cloud", g.Cloud) g.log.Debug("Initializing fleeting-plugin-openstack") - providerClient, endpointOps, err := g.getProviderClient(ctx) - if err != nil { - return provider.ProviderInfo{}, err - } + var err error + g.client, err = newClient(openstackclient.AuthConfig{ + AuthFromEnv: g.AuthFromEnv, + Cloud: g.Cloud, + CloudsConfig: g.CloudsConfig, + NovaMicroversion: g.NovaMicroversion, + }) - cli, err := openstack.NewComputeV2(providerClient, endpointOps) if err != nil { - return provider.ProviderInfo{}, fmt.Errorf("Failed to connect to OpenStack Nova: %w", err) - } - - if g.NovaMicroversion == "" { - g.NovaMicroversion = "2.79" // Train+ - } - - ncli, err := osutil.RequireMicroversion(ctx, *cli, g.NovaMicroversion) - if err != nil { - return provider.ProviderInfo{}, fmt.Errorf("Failed to request microversion %s for OpenStack Nova: %w", g.NovaMicroversion, err) + return provider.ProviderInfo{}, err } - g.computeClient = &ncli - _, err = g.ServerSpec.ToServerCreateMap() if err != nil { return provider.ProviderInfo{}, fmt.Errorf("Failed to check server_spec: %w", err) } if g.ServerSpec.ImageRef != "" { - imgCli, err := openstack.NewImageV2(providerClient, endpointOps) - if err != nil { - return provider.ProviderInfo{}, fmt.Errorf("Failed to get OpenStack Glance: %w", err) - } - - imgProps, err := GetImageProperties(ctx, imgCli, g.ServerSpec.ImageRef) + imgProps, err := g.client.GetImageProperties(ctx, g.ServerSpec.ImageRef) if err != nil { return provider.ProviderInfo{}, err } @@ -126,53 +108,6 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro }, nil } -func (g *InstanceGroup) getProviderClient(ctx context.Context) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { - var endpointOps gophercloud.EndpointOpts - var authOptions gophercloud.AuthOptions - var providerClient *gophercloud.ProviderClient - - if g.AuthFromEnv { - g.log.Debug("Using env vars for auth") - - var err error - endpointOps = gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} - authOptions, err = openstack.AuthOptionsFromEnv() - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to get auth options from environment: %w", err) - } - authOptions.AllowReauth = true - - providerClient, err = openstack.AuthenticatedClient(ctx, authOptions) - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to connect to OpenStack Keystone: %w", err) - } - } else { - g.log.Debug("Using clouds.yaml for auth") - - var err error - var tlsCfg *tls.Config - cloudOpts := []clouds.ParseOption{clouds.WithCloudName(g.Cloud)} - if g.CloudsConfig != "" { - cloudOpts = append(cloudOpts, clouds.WithLocations(g.CloudsConfig)) - } - - authOptions, endpointOps, tlsCfg, err = clouds.Parse(cloudOpts...) - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to parse clouds.yaml: %w", err) - } - - // plugin is a long running process. force allow reauth - authOptions.AllowReauth = true - - providerClient, err = config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsCfg)) - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("Failed to connect to OpenStack Keystone: %w", err) - } - } - - return providerClient, endpointOps, nil -} - func (g *InstanceGroup) Update(ctx context.Context, update func(instance string, state provider.State)) error { instances, err := g.getInstances(ctx) @@ -202,9 +137,7 @@ func (g *InstanceGroup) Update(ctx context.Context, update func(instance string, // treat all nodes running long enough as Running state = provider.StateRunning } else { - log, err := servers.ShowConsoleOutput(ctx, g.computeClient, srv.ID, servers.ShowConsoleOutputOpts{ - Length: 100, - }).Extract() + log, err := g.client.ShowServerConsoleOutput(ctx, srv.ID) if err != nil { reterr = errors.Join(reterr, err) continue @@ -252,7 +185,7 @@ func (g *InstanceGroup) Decrease(ctx context.Context, instances []string) (succe succeeded = make([]string, 0, len(instances)) for _, id := range instances { - err2 := g.deleteInstance(ctx, id) + err2 := g.client.DeleteServer(ctx, id) if err2 != nil { g.log.Error("Failed to delete instance", "err", err2, "id", id) err = errors.Join(err, err2) @@ -268,14 +201,9 @@ func (g *InstanceGroup) Decrease(ctx context.Context, instances []string) (succe } func (g *InstanceGroup) getInstances(ctx context.Context) ([]servers.Server, error) { - page, err := servers.List(g.computeClient, nil).AllPages(ctx) - if err != nil { - return nil, fmt.Errorf("Server listing error: %w", err) - } - - allServers, err := servers.ExtractServers(page) + allServers, err := g.client.ListServers(ctx) if err != nil { - return nil, fmt.Errorf("Server listing extract error: %w", err) + return nil, err } filteredServers := make([]servers.Server, 0, len(allServers)) @@ -318,7 +246,7 @@ func (g *InstanceGroup) createInstance(ctx context.Context) (string, error) { } } - srv, err := servers.Create(ctx, g.computeClient, spec, hintOpts).Extract() + srv, err := g.client.CreateServer(ctx, spec, hintOpts) if err != nil { return "", err } @@ -326,16 +254,8 @@ func (g *InstanceGroup) createInstance(ctx context.Context) (string, error) { return srv.ID, nil } -func (g *InstanceGroup) deleteInstance(ctx context.Context, id string) error { - return servers.Delete(ctx, g.computeClient, id).ExtractErr() -} - -func (g *InstanceGroup) getInstance(ctx context.Context, id string) (*servers.Server, error) { - return servers.Get(ctx, g.computeClient, id).Extract() -} - func (g *InstanceGroup) ConnectInfo(ctx context.Context, instanceID string) (provider.ConnectInfo, error) { - srv, err := g.getInstance(ctx, instanceID) + srv, err := g.client.GetServer(ctx, instanceID) if err != nil { return provider.ConnectInfo{}, fmt.Errorf("Failed to get server %s: %w", instanceID, err) } diff --git a/utils.go b/utils.go index 7f48d19..a0d8fe6 100644 --- a/utils.go +++ b/utils.go @@ -1,7 +1,6 @@ package fpoc import ( - "context" "encoding/json" "fmt" "maps" @@ -11,9 +10,7 @@ import ( igncfg "github.com/coreos/ignition/v2/config/v3_4" igntyp "github.com/coreos/ignition/v2/config/v3_4/types" "github.com/coreos/vcontext/report" - "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" - "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" "github.com/mitchellh/mapstructure" ) @@ -143,34 +140,6 @@ func IsIgnitionFinished(log string) bool { return false } -// Some good known properties useful for setting up ConnectInfo -// -// See also: https://docs.openstack.org/glance/latest/admin/useful-image-properties.html -type ImageProperties struct { - Architecture string `json:"architecture,omitempty" mapstructure:"architecture,omitempty"` - OSType string `json:"os_type,omitempty" mapstructure:"os_type,omitempty"` - OSDistro string `json:"os_distro,omitempty" mapstructure:"os_distro,omitempty"` - OSVersion string `json:"os_version,omitempty" mapstructure:"os_version,omitempty"` - OSAdminUser string `json:"os_admin_user,omitempty" mapstructure:"os_admin_user,omitempty"` - - // Extra map[string]any `mapstructure:",remain"` -} - -func GetImageProperties(ctx context.Context, cli *gophercloud.ServiceClient, imageID string) (*ImageProperties, error) { - image, err := images.Get(ctx, cli, imageID).Extract() - if err != nil { - return nil, fmt.Errorf("failed to get image %s: %w", imageID, err) - } - - out := new(ImageProperties) - err = mapstructure.Decode(image.Properties, out) - if err != nil { - return nil, fmt.Errorf("failed to parse properties: %w", err) - } - - return out, nil -} - func InsertSSHKeyIgn(spec *ExtCreateOpts, username, pubKey string) error { var cfg igntyp.Config var err error diff --git a/utils_test.go b/utils_test.go index 72ffe6d..d69333d 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,13 +1,10 @@ package fpoc import ( - "context" "encoding/json" "os" "testing" - "github.com/gophercloud/gophercloud/v2/testhelper" - tc "github.com/gophercloud/gophercloud/v2/testhelper/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -111,29 +108,6 @@ func TestExtCreateOpts(t *testing.T) { //t.Log(string(req)) } -func TestGetImageProperties(t *testing.T) { - assert := assert.New(t) - - img, err := os.ReadFile("./testdata/image_get.json") - require.NoError(t, err) - - testhelper.SetupHTTP() - defer testhelper.TeardownHTTP() - - testhelper.ServeFile(t, "", "", "application/json", string(img)) - - ctx := context.TODO() - imgCli := tc.ServiceClient() - - props, err := GetImageProperties(ctx, imgCli, "1da9661c-953e-424d-a1e5-834a8174b198") - assert.NoError(err) - if assert.NotNil(props) { - assert.Equal("core", props.OSAdminUser) - } - - t.Log(props) -} - func TestInsertSSHKeyIgn(t *testing.T) { testCases := []struct { name string From 7b4fb49d7e16b76d9c1281dd73d0d135815017ca Mon Sep 17 00:00:00 2001 From: Matthias Baur Date: Tue, 10 Dec 2024 13:11:27 +0100 Subject: [PATCH 3/6] Use given context --- internal/openstackclient/openstackclient.go | 12 ++++++------ provider.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/openstackclient/openstackclient.go b/internal/openstackclient/openstackclient.go index 247490d..753ed5e 100644 --- a/internal/openstackclient/openstackclient.go +++ b/internal/openstackclient/openstackclient.go @@ -66,12 +66,12 @@ type client struct { image *gophercloud.ServiceClient } -func New(authConfig AuthConfig) (Client, error) { +func New(ctx context.Context, authConfig AuthConfig) (Client, error) { if authConfig.NovaMicroversion == "" { authConfig.NovaMicroversion = "2.79" // Train+ } - providerClient, endpointOps, err := newProviderClient(authConfig) + providerClient, endpointOps, err := newProviderClient(ctx, authConfig) if err != nil { return nil, err } @@ -81,7 +81,7 @@ func New(authConfig AuthConfig) (Client, error) { return nil, err } - _computeClient, err := utils.RequireMicroversion(context.TODO(), *computeClient, authConfig.NovaMicroversion) + _computeClient, err := utils.RequireMicroversion(ctx, *computeClient, authConfig.NovaMicroversion) if err != nil { return nil, fmt.Errorf("failed to request microversion %s for OpenStack Nova: %w", authConfig.NovaMicroversion, err) } @@ -99,7 +99,7 @@ func New(authConfig AuthConfig) (Client, error) { }, nil } -func newProviderClient(authConfig AuthConfig) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { +func newProviderClient(ctx context.Context, authConfig AuthConfig) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { var endpointOps gophercloud.EndpointOpts var authOptions gophercloud.AuthOptions var providerClient *gophercloud.ProviderClient @@ -113,7 +113,7 @@ func newProviderClient(authConfig AuthConfig) (*gophercloud.ProviderClient, goph } authOptions.AllowReauth = true - providerClient, err = openstack.AuthenticatedClient(context.Background(), authOptions) + providerClient, err = openstack.AuthenticatedClient(ctx, authOptions) if err != nil { return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) } @@ -133,7 +133,7 @@ func newProviderClient(authConfig AuthConfig) (*gophercloud.ProviderClient, goph // plugin is a long running process. force allow reauth authOptions.AllowReauth = true - providerClient, err = config.NewProviderClient(context.TODO(), authOptions, config.WithTLSConfig(tlsCfg)) + providerClient, err = config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsCfg)) if err != nil { return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) } diff --git a/provider.go b/provider.go index 93263a2..63f1d54 100644 --- a/provider.go +++ b/provider.go @@ -48,7 +48,7 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro g.log.Debug("Initializing fleeting-plugin-openstack") var err error - g.client, err = newClient(openstackclient.AuthConfig{ + g.client, err = newClient(ctx, openstackclient.AuthConfig{ AuthFromEnv: g.AuthFromEnv, Cloud: g.Cloud, CloudsConfig: g.CloudsConfig, From 9899a16116c9fcc01753d66d4e8a25bc18964176 Mon Sep 17 00:00:00 2001 From: Matthias Baur Date: Tue, 10 Dec 2024 15:06:53 +0100 Subject: [PATCH 4/6] Drop newClient --- provider.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/provider.go b/provider.go index 63f1d54..f9f43b8 100644 --- a/provider.go +++ b/provider.go @@ -22,8 +22,6 @@ const MetadataKey = "fleeting-cluster" var _ provider.InstanceGroup = (*InstanceGroup)(nil) -var newClient = openstackclient.New - type InstanceGroup struct { Cloud string `json:"cloud"` // cloud to use CloudsConfig string `json:"clouds_config"` // optional: path to clouds.yaml @@ -48,7 +46,7 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro g.log.Debug("Initializing fleeting-plugin-openstack") var err error - g.client, err = newClient(ctx, openstackclient.AuthConfig{ + g.client, err = openstackclient.New(ctx, openstackclient.AuthConfig{ AuthFromEnv: g.AuthFromEnv, Cloud: g.Cloud, CloudsConfig: g.CloudsConfig, From 87b2a17e19003a0098179e21aaf6a5bc8dc94378 Mon Sep 17 00:00:00 2001 From: Matthias Baur Date: Tue, 10 Dec 2024 15:13:05 +0100 Subject: [PATCH 5/6] Return after env variables --- internal/openstackclient/openstackclient.go | 44 +++++++++------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/internal/openstackclient/openstackclient.go b/internal/openstackclient/openstackclient.go index 753ed5e..bb85dd0 100644 --- a/internal/openstackclient/openstackclient.go +++ b/internal/openstackclient/openstackclient.go @@ -2,7 +2,6 @@ package openstackclient import ( "context" - "crypto/tls" "fmt" "os" @@ -100,43 +99,38 @@ func New(ctx context.Context, authConfig AuthConfig) (Client, error) { } func newProviderClient(ctx context.Context, authConfig AuthConfig) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { - var endpointOps gophercloud.EndpointOpts - var authOptions gophercloud.AuthOptions - var providerClient *gophercloud.ProviderClient - if authConfig.AuthFromEnv { var err error - endpointOps = gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} - authOptions, err = openstack.AuthOptionsFromEnv() + endpointOps := gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} + authOptions, err := openstack.AuthOptionsFromEnv() if err != nil { return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to get auth options from environment: %w", err) } authOptions.AllowReauth = true - providerClient, err = openstack.AuthenticatedClient(ctx, authOptions) + providerClient, err := openstack.AuthenticatedClient(ctx, authOptions) if err != nil { return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) } - } else { - var err error - var tlsCfg *tls.Config - cloudOpts := []clouds.ParseOption{clouds.WithCloudName(authConfig.Cloud)} - if authConfig.CloudsConfig != "" { - cloudOpts = append(cloudOpts, clouds.WithLocations(authConfig.CloudsConfig)) - } + return providerClient, endpointOps, nil + } - authOptions, endpointOps, tlsCfg, err = clouds.Parse(cloudOpts...) - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to parse clouds.yaml: %w", err) - } + cloudOpts := []clouds.ParseOption{clouds.WithCloudName(authConfig.Cloud)} + if authConfig.CloudsConfig != "" { + cloudOpts = append(cloudOpts, clouds.WithLocations(authConfig.CloudsConfig)) + } - // plugin is a long running process. force allow reauth - authOptions.AllowReauth = true + authOptions, endpointOps, tlsCfg, err := clouds.Parse(cloudOpts...) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } - providerClient, err = config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsCfg)) - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) - } + // plugin is a long running process. force allow reauth + authOptions.AllowReauth = true + + providerClient, err := config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsCfg)) + if err != nil { + return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) } return providerClient, endpointOps, nil From 97ed917cf45125254921355bbefc254abda824c3 Mon Sep 17 00:00:00 2001 From: Matthias Baur Date: Wed, 11 Dec 2024 12:44:12 +0100 Subject: [PATCH 6/6] Implement our own environment variable lookup Based on https://gist.github.com/vooon/911c13af31c4aff53477536d8d6afe65 --- go.mod | 2 + go.sum | 4 + internal/openstackclient/openstackclient.go | 190 +++++++++++++++----- provider.go | 14 +- 4 files changed, 160 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index a6d87ff..0b6a68d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/caarlos0/env/v11 v11.2.2 // indirect github.com/coreos/go-json v0.0.0-20231102161613-e49c8866685a // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -28,6 +29,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect diff --git a/go.sum b/go.sum index d9e3d90..d2a81b6 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg= +github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc= github.com/coreos/go-json v0.0.0-20231102161613-e49c8866685a h1:QimUZQ6Au5wFKKkPMmdoXen+CNR66lXt/76AQLBltS0= github.com/coreos/go-json v0.0.0-20231102161613-e49c8866685a/go.mod h1:rcFZM3uxVvdyNmsAV2jopgPD1cs5SPWJWU5dOz2LUnw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= @@ -37,6 +39,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gophercloud/gophercloud/v2 v2.3.0 h1:5ipI2Mgxee0TwQxqnOIUdTbzL4ZBB8GORyZko+yGXI0= github.com/gophercloud/gophercloud/v2 v2.3.0/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4= +github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 h1:N65GYmx5LrMeYdeXcxMESDU+2pDyAOXlFNlHl7siUwM= +github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26/go.mod h1:7SHUbtoiSYINNKgAVxse+PMhIio05IK7shHy8DVRaN0= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= diff --git a/internal/openstackclient/openstackclient.go b/internal/openstackclient/openstackclient.go index bb85dd0..776946c 100644 --- a/internal/openstackclient/openstackclient.go +++ b/internal/openstackclient/openstackclient.go @@ -2,9 +2,11 @@ package openstackclient import ( "context" + "crypto/tls" "fmt" - "os" + "net/http" + "github.com/caarlos0/env/v11" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" @@ -12,21 +14,43 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/config/clouds" "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" "github.com/gophercloud/gophercloud/v2/openstack/utils" + osClient "github.com/gophercloud/utils/v2/client" "github.com/mitchellh/mapstructure" ) -type AuthConfig struct { - // AuthFromEnv specifies whether to use environment variables for auth - AuthFromEnv bool +type AuthConfig interface { + Parse() (gophercloud.AuthOptions, gophercloud.EndpointOpts, *tls.Config, error) + HTTPOpts() (debug bool, computeApiVersion string) +} - // Cloud is the name of the cloud config from clouds.yaml to use - Cloud string +type CloudOpts struct { + AllowReauth bool `envDefault:"true"` +} - // CloudsConfig is the path to the clouds.yaml file - CloudsConfig string +type CloudConfig struct { + ClientConfigFile string `json:"client-config-file" env:"OS_CLIENT_CONFIG_FILE"` + Cloud string `json:"cloud" env:"OS_CLOUD"` + RegionName string `json:"region-name" env:"OS_REGION_NAME"` + EndpointType string `json:"endpoint-type" env:"OS_ENDPOINT_TYPE"` + Debug bool `json:"debug" env:"OS_DEBUG"` + ComputeApiVersion string `json:"compute-api-version" env:"OS_COMPUTE_API_VERSION" envDefault:"2.79"` +} - // NovaMicroversion is the microversion of the OpenStack Nova client. Default 2.79 (which should be ok for Train+) - NovaMicroversion string +type EnvCloudConfig struct { + CloudConfig `embed:"" yaml:",inline"` + + AuthURL string `json:"auth-url" env:"OS_AUTH_URL"` + Username string `json:"username" env:"OS_USERNAME"` + UserID string `json:"user-id" env:"OS_USER_ID"` + Password string `json:"password" env:"OS_PASSWORD"` + Passcode string `json:"passcode" env:"OS_PASSCODE"` + ProjectName string `json:"project-name" env:"OS_PROJECT_NAME"` + ProjectID string `json:"project-id" env:"OS_PROJECT_ID"` + UserDomainName string `json:"user-domain-name" env:"OS_USER_DOMAIN_NAME"` + UserDomainID string `json:"user-domain-id" env:"OS_USER_DOMAIN_ID"` + ApplicationCredentialID string `json:"application-credential-id" env:"OS_APPLICATION_CREDENTIAL_ID"` + ApplicationCredentialName string `json:"application-credential-name" env:"OS_APPLICATION_CREDENTIAL_NAME"` + ApplicationCredentialSecret string `json:"application-credential-secret" env:"OS_APPLICATION_CREDENTIAL_SECRET"` } // Some good known properties useful for setting up ConnectInfo @@ -58,34 +82,36 @@ type Client interface { DeleteServer(ctx context.Context, serverId string) error } -var _ Client = (*client)(nil) - type client struct { compute *gophercloud.ServiceClient image *gophercloud.ServiceClient } -func New(ctx context.Context, authConfig AuthConfig) (Client, error) { - if authConfig.NovaMicroversion == "" { - authConfig.NovaMicroversion = "2.79" // Train+ +func New(ctx context.Context, authConfig AuthConfig, cloudOpts *CloudOpts) (Client, error) { + if cloudOpts == nil { + cloudOpts = &CloudOpts{} } - providerClient, endpointOps, err := newProviderClient(ctx, authConfig) + var err error + err = env.Parse(cloudOpts) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse cloudOpts: %w", err) } - computeClient, err := openstack.NewComputeV2(providerClient, endpointOps) + err = env.Parse(authConfig) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse authConfig: %w", err) } - _computeClient, err := utils.RequireMicroversion(ctx, *computeClient, authConfig.NovaMicroversion) + providerClient, endpointOps, err := NewProviderClient(ctx, authConfig, cloudOpts) if err != nil { - return nil, fmt.Errorf("failed to request microversion %s for OpenStack Nova: %w", authConfig.NovaMicroversion, err) + return nil, err } - computeClient = &_computeClient + computeClient, err := NewComputeClient(ctx, providerClient, endpointOps, authConfig) + if err != nil { + return nil, err + } imageClient, err := openstack.NewImageV2(providerClient, endpointOps) if err != nil { @@ -98,42 +124,120 @@ func New(ctx context.Context, authConfig AuthConfig) (Client, error) { }, nil } -func newProviderClient(ctx context.Context, authConfig AuthConfig) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { - if authConfig.AuthFromEnv { - var err error - endpointOps := gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} - authOptions, err := openstack.AuthOptionsFromEnv() +func (cloudConfig *CloudConfig) HTTPOpts() (debug bool, computeApiVersion string) { + return cloudConfig.Debug, cloudConfig.ComputeApiVersion +} + +func (cloudConfig *CloudConfig) Parse() (gophercloud.AuthOptions, gophercloud.EndpointOpts, *tls.Config, error) { + parseOpts := []clouds.ParseOption{clouds.WithCloudName(cloudConfig.Cloud)} + if cloudConfig.ClientConfigFile != "" { + parseOpts = append(parseOpts, clouds.WithLocations(cloudConfig.ClientConfigFile)) + } + + authOptions, endpointOpts, tlsCfg, err := clouds.Parse(parseOpts...) + if err != nil { + return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } + + if cloudConfig.RegionName != "" { + endpointOpts.Region = cloudConfig.RegionName + } + if cloudConfig.EndpointType != "" { + endpointOpts.Availability = gophercloud.Availability(cloudConfig.EndpointType) + } + + return authOptions, endpointOpts, tlsCfg, nil +} + +func (envCloudConfig *EnvCloudConfig) Parse() (gophercloud.AuthOptions, gophercloud.EndpointOpts, *tls.Config, error) { + if envCloudConfig.Cloud != "" { + authOptions, endpointOpts, tlsCfg, err := envCloudConfig.CloudConfig.Parse() if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to get auth options from environment: %w", err) + return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, err } - authOptions.AllowReauth = true - providerClient, err := openstack.AuthenticatedClient(ctx, authOptions) - if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) + if envCloudConfig.ProjectName != "" { + authOptions.TenantName = envCloudConfig.ProjectName + authOptions.TenantID = "" } - return providerClient, endpointOps, nil + if envCloudConfig.ProjectID != "" { + authOptions.TenantID = envCloudConfig.ProjectID + authOptions.TenantName = "" + } + + return authOptions, endpointOpts, tlsCfg, nil + } + + authOptions := gophercloud.AuthOptions{ + IdentityEndpoint: envCloudConfig.AuthURL, + UserID: envCloudConfig.UserID, + Username: envCloudConfig.Username, + Password: envCloudConfig.Password, + Passcode: envCloudConfig.Passcode, + TenantID: envCloudConfig.ProjectID, + TenantName: envCloudConfig.ProjectName, + DomainID: envCloudConfig.UserDomainID, + DomainName: envCloudConfig.UserDomainName, + ApplicationCredentialID: envCloudConfig.ApplicationCredentialID, + ApplicationCredentialName: envCloudConfig.ApplicationCredentialName, + ApplicationCredentialSecret: envCloudConfig.ApplicationCredentialSecret, + } + + endpointOpts := gophercloud.EndpointOpts{ + Region: envCloudConfig.RegionName, + Availability: gophercloud.Availability(envCloudConfig.EndpointType), } - cloudOpts := []clouds.ParseOption{clouds.WithCloudName(authConfig.Cloud)} - if authConfig.CloudsConfig != "" { - cloudOpts = append(cloudOpts, clouds.WithLocations(authConfig.CloudsConfig)) + return authOptions, endpointOpts, nil, nil +} + +func NewHTTPClient(tlsCfg *tls.Config) http.Client { + httpClient := http.Client{ + Transport: http.DefaultTransport.(*http.Transport).Clone(), + } + + if tlsCfg != nil { + tr := httpClient.Transport.(*http.Transport) + tr.TLSClientConfig = tlsCfg } - authOptions, endpointOps, tlsCfg, err := clouds.Parse(cloudOpts...) + httpClient.Transport = &osClient.RoundTripper{ + Rt: httpClient.Transport, + } + return httpClient +} + +func NewProviderClient(ctx context.Context, authConfig AuthConfig, cloudOpts *CloudOpts) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { + authOptions, endpointOpts, tlsCfg, err := authConfig.Parse() + if err != nil { + return nil, gophercloud.EndpointOpts{}, err + } + + httpClient := NewHTTPClient(tlsCfg) + authOptions.AllowReauth = cloudOpts.AllowReauth + + providerClient, err := config.NewProviderClient(ctx, authOptions, config.WithHTTPClient(httpClient)) if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to parse clouds.yaml: %w", err) + return nil, gophercloud.EndpointOpts{}, err } - // plugin is a long running process. force allow reauth - authOptions.AllowReauth = true + return providerClient, endpointOpts, nil +} + +func NewComputeClient(ctx context.Context, providerClient *gophercloud.ProviderClient, endpointOps gophercloud.EndpointOpts, authConfig AuthConfig) (*gophercloud.ServiceClient, error) { + _, computeApiVersion := authConfig.HTTPOpts() + + computeClient, err := openstack.NewComputeV2(providerClient, endpointOps) + if err != nil { + return &gophercloud.ServiceClient{}, err + } - providerClient, err := config.NewProviderClient(ctx, authOptions, config.WithTLSConfig(tlsCfg)) + _computeClient, err := utils.RequireMicroversion(ctx, *computeClient, computeApiVersion) if err != nil { - return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) + return &gophercloud.ServiceClient{}, err } - return providerClient, endpointOps, nil + return &_computeClient, err } func (c *client) GetImageProperties(ctx context.Context, imageRef string) (*ImageProperties, error) { diff --git a/provider.go b/provider.go index f9f43b8..65b8bdf 100644 --- a/provider.go +++ b/provider.go @@ -25,7 +25,6 @@ var _ provider.InstanceGroup = (*InstanceGroup)(nil) type InstanceGroup struct { Cloud string `json:"cloud"` // cloud to use CloudsConfig string `json:"clouds_config"` // optional: path to clouds.yaml - AuthFromEnv bool `json:"auth_from_env"` // optional: Use environment variables for authentication Name string `json:"name"` // name of the cluster NovaMicroversion string `json:"nova_microversion"` // Microversion for the Nova client ServerSpec ExtCreateOpts `json:"server_spec"` // instance creation spec @@ -46,12 +45,13 @@ func (g *InstanceGroup) Init(ctx context.Context, log hclog.Logger, settings pro g.log.Debug("Initializing fleeting-plugin-openstack") var err error - g.client, err = openstackclient.New(ctx, openstackclient.AuthConfig{ - AuthFromEnv: g.AuthFromEnv, - Cloud: g.Cloud, - CloudsConfig: g.CloudsConfig, - NovaMicroversion: g.NovaMicroversion, - }) + g.client, err = openstackclient.New(ctx, &openstackclient.EnvCloudConfig{ + CloudConfig: openstackclient.CloudConfig{ + ClientConfigFile: g.CloudsConfig, + Cloud: g.Cloud, + ComputeApiVersion: g.NovaMicroversion, + }, + }, nil) if err != nil { return provider.ProviderInfo{}, err