Files
cloud-ip-validator/internal/openstack/client.go
T
2026-08-21 07:34:45 +03:00

105 lines
3.4 KiB
Go

package openstack
import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/v2"
osauth "github.com/gophercloud/gophercloud/v2/openstack"
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips"
)
// ClientConfig carries pre-resolved credential values (already read from
// environment variables by the caller — see config.OpenStackAuth). Token
// auth is required per the deployment constraint that the admin credential
// is supplied via the process environment, not a config file.
type ClientConfig struct {
AuthURL string
Token string
ProjectID string
ProjectName string
DomainName string
Region string
}
type Client struct {
networking *gophercloud.ServiceClient
}
// NewClient authenticates against Keystone using a pre-issued admin token
// and returns a Client scoped to the given project/region, backed by the
// Neutron (networking v2) service catalog entry.
func NewClient(ctx context.Context, cfg ClientConfig) (*Client, error) {
if cfg.AuthURL == "" || cfg.Token == "" {
return nil, fmt.Errorf("openstack: auth URL and token are required")
}
authOpts := gophercloud.AuthOptions{
IdentityEndpoint: cfg.AuthURL,
TokenID: cfg.Token,
TenantID: cfg.ProjectID,
TenantName: cfg.ProjectName,
DomainName: cfg.DomainName,
}
if cfg.ProjectID != "" || cfg.ProjectName != "" {
authOpts.Scope = &gophercloud.AuthScope{
ProjectID: cfg.ProjectID,
ProjectName: cfg.ProjectName,
DomainName: cfg.DomainName,
}
}
provider, err := osauth.AuthenticatedClient(ctx, authOpts)
if err != nil {
return nil, fmt.Errorf("openstack: authenticate: %w", err)
}
networking, err := osauth.NewNetworkV2(provider, gophercloud.EndpointOpts{Region: cfg.Region})
if err != nil {
return nil, fmt.Errorf("openstack: networking client: %w", err)
}
return &Client{networking: networking}, nil
}
func (c *Client) GetFloatingIPByAddress(ctx context.Context, address string) (*FloatingIP, error) {
pages, err := floatingips.List(c.networking, floatingips.ListOpts{FloatingIP: address}).AllPages(ctx)
if err != nil {
return nil, fmt.Errorf("openstack: list floating ips: %w", err)
}
list, err := floatingips.ExtractFloatingIPs(pages)
if err != nil {
return nil, fmt.Errorf("openstack: extract floating ips: %w", err)
}
if len(list) == 0 {
return nil, ErrNotFound(address)
}
f := list[0]
return &FloatingIP{ID: f.ID, Address: f.FloatingIP, PortID: f.PortID, ProjectID: f.TenantID}, nil
}
func (c *Client) AssociateFloatingIP(ctx context.Context, fipID, portID string) error {
_, err := floatingips.Update(ctx, c.networking, fipID, floatingips.UpdateOpts{
PortID: &portID,
}).Extract()
if err != nil {
return fmt.Errorf("openstack: associate floating ip %s -> port %s: %w", fipID, portID, err)
}
return nil
}
func (c *Client) DisassociateFloatingIP(ctx context.Context, fipID string) error {
// gophercloud's UpdateOpts.PortID is *string with `omitempty`: a nil
// pointer is dropped from the request body entirely (no-op), while a
// pointer to "" is what actually serializes as port_id:null and
// disassociates the floating IP. See floatingips.UpdateOpts godoc.
empty := ""
_, err := floatingips.Update(ctx, c.networking, fipID, floatingips.UpdateOpts{
PortID: &empty,
}).Extract()
if err != nil {
return fmt.Errorf("openstack: disassociate floating ip %s: %w", fipID, err)
}
return nil
}