mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
8da6a23aa5
terraform: collect apiserver cert SANs and support custom endpoint constants: add new constants for cluster configuration and custom endpoint cloud: support apiserver cert sans and prepare for endpoint migration on AWS config: add customEndpoint field bootstrapper: use per-CSP apiserver cert SANs cli: route customEndpoint to terraform and add migration for apiserver cert SANs bootstrapper: change interface of GetLoadBalancerEndpoint to return host and port separately
357 lines
8.5 KiB
Go
357 lines
8.5 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package openstack
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
|
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
|
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
|
|
"github.com/gophercloud/utils/openstack/clientconfig"
|
|
)
|
|
|
|
const (
|
|
roleTagFormat = "constellation-role-%s"
|
|
microversion = "2.42"
|
|
)
|
|
|
|
// Cloud is the metadata client for OpenStack.
|
|
type Cloud struct {
|
|
api serversAPI
|
|
imds imdsAPI
|
|
}
|
|
|
|
// New creates a new OpenStack metadata client.
|
|
func New(ctx context.Context) (*Cloud, error) {
|
|
imds := &imdsClient{client: &http.Client{}}
|
|
|
|
authURL, err := imds.authURL(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting auth URL: %w", err)
|
|
}
|
|
username, err := imds.username(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting token name: %w", err)
|
|
}
|
|
password, err := imds.password(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting token password: %w", err)
|
|
}
|
|
userDomainName, err := imds.userDomainName(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting user domain name: %w", err)
|
|
}
|
|
|
|
clientOpts := &clientconfig.ClientOpts{
|
|
AuthType: clientconfig.AuthV3Password,
|
|
AuthInfo: &clientconfig.AuthInfo{
|
|
AuthURL: authURL,
|
|
UserDomainName: userDomainName,
|
|
Username: username,
|
|
Password: password,
|
|
},
|
|
}
|
|
|
|
serversClient, err := clientconfig.NewServiceClient("compute", clientOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating compute client: %w", err)
|
|
}
|
|
serversClient.Microversion = microversion
|
|
|
|
subnetsClient, err := clientconfig.NewServiceClient("network", clientOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating network client: %w", err)
|
|
}
|
|
subnetsClient.Microversion = microversion
|
|
|
|
return &Cloud{
|
|
imds: imds,
|
|
api: &apiClient{
|
|
servers: serversClient,
|
|
subnets: subnetsClient,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Self returns the metadata of the current instance.
|
|
func (c *Cloud) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
|
|
name, err := c.imds.name(ctx)
|
|
if err != nil {
|
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting name: %w", err)
|
|
}
|
|
providerID, err := c.imds.providerID(ctx)
|
|
if err != nil {
|
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting provider id: %w", err)
|
|
}
|
|
role, err := c.imds.role(ctx)
|
|
if err != nil {
|
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting role: %w", err)
|
|
}
|
|
vpcIP, err := c.imds.vpcIP(ctx)
|
|
if err != nil {
|
|
return metadata.InstanceMetadata{}, fmt.Errorf("getting vpc ip: %w", err)
|
|
}
|
|
|
|
return metadata.InstanceMetadata{
|
|
Name: name,
|
|
ProviderID: providerID,
|
|
Role: role,
|
|
VPCIP: vpcIP,
|
|
}, nil
|
|
}
|
|
|
|
// List returns the metadata of all instances belonging to the same Constellation cluster.
|
|
func (c *Cloud) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
|
|
uid, err := c.imds.uid(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting uid: %w", err)
|
|
}
|
|
|
|
uidTag := fmt.Sprintf("constellation-uid-%s", uid)
|
|
|
|
subnet, err := c.getSubnetCIDR(uidTag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
srvs, err := c.getServers(uidTag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result []metadata.InstanceMetadata
|
|
for _, s := range srvs {
|
|
if s.Name == "" {
|
|
continue
|
|
}
|
|
if s.ID == "" {
|
|
continue
|
|
}
|
|
if s.Tags == nil {
|
|
continue
|
|
}
|
|
|
|
var serverRole role.Role
|
|
for _, t := range *s.Tags {
|
|
if strings.HasPrefix(t, "constellation-role-") {
|
|
serverRole = role.FromString(strings.TrimPrefix(t, "constellation-role-"))
|
|
break
|
|
}
|
|
}
|
|
if serverRole == role.Unknown {
|
|
continue
|
|
}
|
|
|
|
subnetAddrs, err := parseSeverAddresses(s.Addresses)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing server %q addresses: %w", s.Name, err)
|
|
}
|
|
|
|
var vpcIP string
|
|
// In a best effort approach, we take the first fixed IPv4 address that is in the subnet
|
|
// belonging to our cluster.
|
|
for _, serverSubnet := range subnetAddrs {
|
|
for _, addr := range serverSubnet.Addresses {
|
|
if addr.Type != fixedIP {
|
|
continue
|
|
}
|
|
|
|
if addr.IPVersion != ipV4 {
|
|
continue
|
|
}
|
|
|
|
if addr.IP == "" {
|
|
continue
|
|
}
|
|
|
|
parsedAddr, err := netip.ParseAddr(addr.IP)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if !subnet.Contains(parsedAddr) {
|
|
continue
|
|
}
|
|
|
|
vpcIP = addr.IP
|
|
break
|
|
}
|
|
}
|
|
if vpcIP == "" {
|
|
continue
|
|
}
|
|
|
|
im := metadata.InstanceMetadata{
|
|
Name: s.Name,
|
|
ProviderID: s.ID,
|
|
Role: serverRole,
|
|
VPCIP: vpcIP,
|
|
}
|
|
result = append(result, im)
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return nil, fmt.Errorf("no instances belonging to this cluster found")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// UID retrieves the UID of the constellation.
|
|
func (c *Cloud) UID(ctx context.Context) (string, error) {
|
|
uid, err := c.imds.uid(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("retrieving instance UID: %w", err)
|
|
}
|
|
return uid, nil
|
|
}
|
|
|
|
// InitSecretHash retrieves the InitSecretHash of the current instance.
|
|
func (c *Cloud) InitSecretHash(ctx context.Context) ([]byte, error) {
|
|
initSecretHash, err := c.imds.initSecretHash(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving init secret hash: %w", err)
|
|
}
|
|
return []byte(initSecretHash), nil
|
|
}
|
|
|
|
// GetLoadBalancerEndpoint returns the endpoint of the load balancer.
|
|
// For OpenStack, the load balancer is a floating ip attached to
|
|
// a control plane node.
|
|
// TODO(malt3): Rewrite to use real load balancer once it is available.
|
|
func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (host, port string, err error) {
|
|
host, err = c.getLoadBalancerHost(ctx)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("getting load balancer host: %w", err)
|
|
}
|
|
return host, strconv.FormatInt(constants.KubernetesPort, 10), nil
|
|
}
|
|
|
|
func (c *Cloud) getLoadBalancerHost(ctx context.Context) (string, error) {
|
|
uid, err := c.imds.uid(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting uid: %w", err)
|
|
}
|
|
|
|
uidTag := fmt.Sprintf("constellation-uid-%s", uid)
|
|
|
|
subnet, err := c.getSubnetCIDR(uidTag)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
srvs, err := c.getServers(uidTag)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, s := range srvs {
|
|
if s.Name == "" {
|
|
continue
|
|
}
|
|
if s.ID == "" {
|
|
continue
|
|
}
|
|
if s.Tags == nil {
|
|
continue
|
|
}
|
|
|
|
subnetAddrs, err := parseSeverAddresses(s.Addresses)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing server %q addresses: %w", s.Name, err)
|
|
}
|
|
|
|
// In a best effort approach, we take the first fixed IPv4 address that is outside the subnet
|
|
// belonging to our cluster and assume it is the "load balancer" floating ip.
|
|
for _, serverSubnet := range subnetAddrs {
|
|
for _, addr := range serverSubnet.Addresses {
|
|
if addr.Type != floatingIP {
|
|
continue
|
|
}
|
|
|
|
if addr.IPVersion != ipV4 {
|
|
continue
|
|
}
|
|
|
|
if addr.IP == "" {
|
|
continue
|
|
}
|
|
|
|
parsedAddr, err := netip.ParseAddr(addr.IP)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if subnet.Contains(parsedAddr) {
|
|
continue
|
|
}
|
|
|
|
return addr.IP, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", errors.New("no load balancer endpoint found")
|
|
}
|
|
|
|
// GetNetworkIDs returns the IDs of the networks the current instance is part of.
|
|
// This method is OpenStack specific.
|
|
func (c *Cloud) GetNetworkIDs(ctx context.Context) ([]string, error) {
|
|
networkIDs, err := c.imds.networkIDs(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retrieving network IDs: %w", err)
|
|
}
|
|
return networkIDs, nil
|
|
}
|
|
|
|
func (c *Cloud) getSubnetCIDR(uidTag string) (netip.Prefix, error) {
|
|
listSubnetsOpts := subnets.ListOpts{Tags: uidTag}
|
|
subnetsPage, err := c.api.ListSubnets(listSubnetsOpts).AllPages()
|
|
if err != nil {
|
|
return netip.Prefix{}, fmt.Errorf("listing subnets: %w", err)
|
|
}
|
|
|
|
nets, err := subnets.ExtractSubnets(subnetsPage)
|
|
if err != nil {
|
|
return netip.Prefix{}, fmt.Errorf("extracting subnets: %w", err)
|
|
}
|
|
|
|
if len(nets) != 1 {
|
|
return netip.Prefix{}, fmt.Errorf("expected exactly one subnet, got %d", len(nets))
|
|
}
|
|
|
|
cidr, err := netip.ParsePrefix(nets[0].CIDR)
|
|
if err != nil {
|
|
return netip.Prefix{}, fmt.Errorf("parsing subnet CIDR: %w", err)
|
|
}
|
|
|
|
return cidr, nil
|
|
}
|
|
|
|
func (c *Cloud) getServers(uidTag string) ([]servers.Server, error) {
|
|
listServersOpts := servers.ListOpts{Tags: uidTag}
|
|
serversPage, err := c.api.ListServers(listServersOpts).AllPages()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing servers: %w", err)
|
|
}
|
|
servers, err := servers.ExtractServers(serversPage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extracting servers: %w", err)
|
|
}
|
|
|
|
return servers, nil
|
|
}
|