diff --git a/README.md b/README.md index ad9f6cb..1ec6492 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 c6cd717..2b3d264 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)