From e18f3433c2fe468e58fd56bdfdc0f25a1a690ca2 Mon Sep 17 00:00:00 2001 From: Jimmi Dyson Date: Mon, 22 Jul 2024 16:23:17 +0100 Subject: [PATCH] feat: Introduce adapter client --- adapter/client.go | 32 +++++ adapter/cluster.go | 130 ++++++++++++++++++ adapter/networking.go | 308 ++++++++++++++++++++++++++++++++++++++++++ adapter/prism.go | 65 +++++++++ 4 files changed, 535 insertions(+) create mode 100644 adapter/client.go create mode 100644 adapter/cluster.go create mode 100644 adapter/networking.go create mode 100644 adapter/prism.go diff --git a/adapter/client.go b/adapter/client.go new file mode 100644 index 00000000..e4cd84a4 --- /dev/null +++ b/adapter/client.go @@ -0,0 +1,32 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package adapter + +import ( + "fmt" + + v4 "github.com/nutanix-cloud-native/prism-go-client/v4" +) + +var v4ClientCache = v4.NewClientCache(v4.WithSessionAuth(true)) + +type CachedClientParams = v4.CachedClientParams + +func GetClient(cachedClientParams CachedClientParams) (Client, error) { + v4Client, err := v4ClientCache.GetOrCreate(cachedClientParams) + if err != nil { + return nil, fmt.Errorf("failed to create v4 API client: %w", err) + } + return &client{v4Client: v4Client}, nil +} + +type Client interface { + Networking() NetworkingClient + Prism() PrismClient + Cluster() ClusterClient +} + +type client struct { + v4Client *v4.Client +} diff --git a/adapter/cluster.go b/adapter/cluster.go new file mode 100644 index 00000000..a578fa25 --- /dev/null +++ b/adapter/cluster.go @@ -0,0 +1,130 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package adapter + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/nutanix-cloud-native/prism-go-client/utils" + v4 "github.com/nutanix-cloud-native/prism-go-client/v4" + clustersapi "github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4/models/clustermgmt/v4/config" +) + +type ClusterClient interface { + GetCluster(cluster string) (*Cluster, error) +} + +type Cluster struct { + extID uuid.UUID +} + +func (c *Cluster) ExtID() uuid.UUID { + return c.extID +} + +func (c *client) Cluster() ClusterClient { + return &clusterClient{ + v4Client: c.v4Client, + client: c, + } +} + +type clusterClient struct { + v4Client *v4.Client + client Client +} + +func (n *clusterClient) GetCluster(cluster string) (*Cluster, error) { + clusterUUID, err := uuid.Parse(cluster) + if err == nil { + return n.getClusterByExtID(clusterUUID) + } + + return n.getClusterByName(cluster) +} + +func (n *clusterClient) getClusterByName(clusterName string) (*Cluster, error) { + response, err := n.v4Client.ClustersApiInstance.ListClusters( + nil, + nil, + utils.StringPtr(fmt.Sprintf(`name eq '%s'`, clusterName)), + nil, + nil, + nil, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to find cluster uuid for cluster %s: %w", + clusterName, + err, + ) + } + clusters := response.GetData() + if clusters == nil { + return nil, fmt.Errorf("no cluster found with name %q", clusterName) + } + + switch apiClusters := clusters.(type) { + case []clustersapi.Cluster: + if len(apiClusters) == 0 { + return nil, fmt.Errorf("no cluster found with name %q", clusterName) + } + if len(apiClusters) > 1 { + return nil, fmt.Errorf("multiple clusters (%d) found with name %q", len(apiClusters), clusterName) + } + + extID := *apiClusters[0].ExtId + clusterUUID, err := uuid.Parse(extID) + if err != nil { + return nil, fmt.Errorf("failed to parse cluster uuid %q for cluster %q: %w", extID, clusterName, err) + } + + return &Cluster{ + extID: clusterUUID, + }, nil + default: + return nil, fmt.Errorf("unknown response: %+v", clusters) + } +} + +func (n *clusterClient) getClusterByExtID(clusterExtID uuid.UUID) (*Cluster, error) { + response, err := n.v4Client.ClustersApiInstance.GetClusterById( + utils.StringPtr(clusterExtID.String()), + ) + if err != nil { + return nil, fmt.Errorf( + "failed to find cluster with extID %q: %w", + clusterExtID, + err, + ) + } + cluster := response.GetData() + if cluster == nil { + return nil, fmt.Errorf("no cluster found with extID %q", clusterExtID) + } + + switch apiCluster := cluster.(type) { + case *clustersapi.Cluster: + if apiCluster.ExtId == nil { + return nil, fmt.Errorf("no extID found for cluster %q", clusterExtID) + } + clusterUUID, err := uuid.Parse(*apiCluster.ExtId) + if err != nil { + return nil, + fmt.Errorf( + "failed to parse cluster extID %q for cluster %q: %w", + *apiCluster.ExtId, + clusterExtID, + err, + ) + } + + return &Cluster{ + extID: clusterUUID, + }, nil + default: + return nil, fmt.Errorf("unknown response: %+v", cluster) + } +} diff --git a/adapter/networking.go b/adapter/networking.go new file mode 100644 index 00000000..81accb2b --- /dev/null +++ b/adapter/networking.go @@ -0,0 +1,308 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package adapter + +import ( + "context" + "encoding/json" + "fmt" + "net" + "strconv" + + "github.com/google/uuid" + "github.com/nutanix-cloud-native/prism-go-client/utils" + networkingcommonapi "github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4/models/common/v1/config" + networkingapi "github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4/models/networking/v4/config" + networkingprismapi "github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4/models/prism/v4/config" +) + +type NetworkingClient interface { + ReserveIP(ctx context.Context, subnet string, opts ReserveIPOpts) (net.IP, error) + UnreserveIP(ctx context.Context, ip net.IP, subnet string, opts UnreserveIPOpts) error + GetSubnet(subnet string, opts GetSubnetOpts) (*Subnet, error) +} + +func (c *client) Networking() NetworkingClient { + return &networkingClient{ + client: c, + } +} + +type networkingClient struct { + *client +} + +type ReserveIPOpts struct { + Cluster string +} + +func (n *networkingClient) ReserveIP(ctx context.Context, subnet string, opts ReserveIPOpts) (ip net.IP, err error) { + subnetUUID, err := uuid.Parse(subnet) + if err != nil { + apiSubnet, err := n.GetSubnet(subnet, GetSubnetOpts{Cluster: opts.Cluster}) + if err != nil { + return nil, fmt.Errorf("failed to get subnet %s: %w", subnet, err) + } + + subnetUUID = apiSubnet.ExtID() + } + + reserveType := networkingapi.RESERVETYPE_IP_ADDRESS_COUNT + reserveIPResponse, err := n.v4Client.SubnetIPReservationApi.ReserveIpsBySubnetId( + utils.StringPtr(subnetUUID.String()), + &networkingapi.IpReserveSpec{ + Count: utils.Int64Ptr(1), + ReserveType: &reserveType, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to reserve IP in subnet %s: %w", subnet, err) + } + + responseData, ok := reserveIPResponse.GetData().(networkingprismapi.TaskReference) + if !ok { + return nil, fmt.Errorf( + "unexpected response data type %[1]T: %+[1]v", + reserveIPResponse.GetData(), + ) + } + if responseData.ExtId == nil { + return nil, fmt.Errorf( + "no task id found in response: %+[1]v", + reserveIPResponse.GetData(), + ) + } + + result, err := n.client.Prism().WaitForTaskCompletion(ctx, *responseData.ExtId) + if err != nil { + return nil, fmt.Errorf("failed to wait for task completion: %w", err) + } + + if len(result) == 0 { + return nil, fmt.Errorf("no IP address reserved") + } + if len(result) > 1 { + return nil, fmt.Errorf("unexpected multiple results returned: %+v", result) + } + + marshaledResponseBytes, _ := json.Marshal(result[0].Value) + marshaledResponse, err := strconv.Unquote(string(marshaledResponseBytes)) + if err != nil { + return nil, fmt.Errorf( + "failed to unquote reserved IP response %s: %w", + marshaledResponseBytes, + err, + ) + } + + type reservedIPs struct { + ReservedIPs []string `json:"reserved_ips"` + } + + var response reservedIPs + if err := json.Unmarshal([]byte(marshaledResponse), &response); err != nil { + return nil, fmt.Errorf( + "failed to unmarshal reserved IP response %s: %w", + marshaledResponse, + err, + ) + } + + if len(response.ReservedIPs) == 0 { + return nil, fmt.Errorf("no IP address reserved") + } + if len(response.ReservedIPs) > 1 { + return nil, fmt.Errorf("unexpected multiple IPs reserved: %+v", response.ReservedIPs) + } + + reservedIP := net.ParseIP(response.ReservedIPs[0]) + if reservedIP == nil { + return nil, fmt.Errorf("failed to parse reserved IP %q", response.ReservedIPs[0]) + } + + return reservedIP, nil +} + +type UnreserveIPOpts = ReserveIPOpts + +func (n *networkingClient) UnreserveIP(ctx context.Context, ip net.IP, subnet string, opts UnreserveIPOpts) error { + subnetUUID, err := uuid.Parse(subnet) + if err != nil { + apiSubnet, err := n.GetSubnet(subnet, GetSubnetOpts{Cluster: opts.Cluster}) + if err != nil { + return fmt.Errorf("failed to get subnet %s: %w", subnet, err) + } + + subnetUUID = apiSubnet.ExtID() + } + + ipAddress := networkingcommonapi.NewIPAddress() + if ip.To4() != nil { + ipAddress.Ipv4 = networkingcommonapi.NewIPv4Address() + ipAddress.Ipv4.Value = utils.StringPtr(ip.String()) + } else { + ipAddress.Ipv6 = networkingcommonapi.NewIPv6Address() + ipAddress.Ipv6.Value = utils.StringPtr(ip.String()) + } + + unreserveType := networkingapi.UNRESERVETYPE_IP_ADDRESS_LIST + unreserveIPResponse, err := n.v4Client.SubnetIPReservationApi.UnreserveIpsBySubnetId( + utils.StringPtr(subnetUUID.String()), + &networkingapi.IpUnreserveSpec{ + UnreserveType: &unreserveType, + IpAddresses: []networkingcommonapi.IPAddress{*ipAddress}, + }, + ) + if err != nil { + return fmt.Errorf("failed to unreserve IP in subnet %s: %w", subnet, err) + } + + responseData, ok := unreserveIPResponse.GetData().(networkingprismapi.TaskReference) + if !ok { + return fmt.Errorf( + "unexpected response data type %[1]T: %+[1]v", + unreserveIPResponse.GetData(), + ) + } + if responseData.ExtId == nil { + return fmt.Errorf("no task id found in response: %+v", unreserveIPResponse.GetData()) + } + + _, err = n.client.Prism().WaitForTaskCompletion(ctx, *responseData.ExtId) + if err != nil { + return fmt.Errorf("failed to wait for task completion: %w", err) + } + + return nil +} + +type GetSubnetOpts = ReserveIPOpts + +type Subnet struct { + extID uuid.UUID +} + +func (s *Subnet) ExtID() uuid.UUID { + return s.extID +} + +func (n *networkingClient) GetSubnet(subnet string, opts GetSubnetOpts) (*Subnet, error) { + subnetUUID, err := uuid.Parse(subnet) + if err == nil { + return n.getSubnetByExtID(subnetUUID) + } + + return n.getSubnetByName(subnet, opts) +} + +func (n *networkingClient) getSubnetByName(subnetName string, opts GetSubnetOpts) (*Subnet, error) { + filter := fmt.Sprintf(`name eq '%s'`, subnetName) + if opts.Cluster != "" { + apiCluster, err := n.client.Cluster().GetCluster(opts.Cluster) + if err != nil { + return nil, fmt.Errorf("failed to get cluster %s: %w", opts.Cluster, err) + } + + filter += fmt.Sprintf(` and clusterReference eq '%s'`, apiCluster.ExtID()) + } + + response, err := n.v4Client.SubnetsApiInstance.ListSubnets( + nil, + nil, + utils.StringPtr(filter), + nil, + nil, + nil, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to find subnet uuid for subnet %q: %w", + subnetName, + err, + ) + } + subnets := response.GetData() + if subnets == nil { + return nil, fmt.Errorf("no subnet found with name %q", subnetName) + } + + switch apiSubnets := subnets.(type) { + case []networkingapi.Subnet: + if len(apiSubnets) == 0 { + return nil, fmt.Errorf("no subnet found with name %q", subnetName) + } + if len(apiSubnets) > 1 { + return nil, fmt.Errorf("multiple subnets (%d) found with name %q", len(apiSubnets), subnetName) + } + + extID := *apiSubnets[0].ExtId + subnetUUID, err := uuid.Parse(extID) + if err != nil { + return nil, fmt.Errorf("failed to parse subnet uuid %q for cluster %q: %w", extID, opts.Cluster, err) + } + + return &Subnet{ + extID: subnetUUID, + }, nil + case []networkingapi.SubnetProjection: + if len(apiSubnets) == 0 { + return nil, fmt.Errorf("no subnet found with name %s", subnetName) + } + if len(apiSubnets) > 1 { + return nil, fmt.Errorf("multiple subnets (%d) found with name %q", len(apiSubnets), subnetName) + } + + extID := *apiSubnets[0].ExtId + subnetUUID, err := uuid.Parse(extID) + if err != nil { + return nil, fmt.Errorf("failed to parse subnet uuid %q for cluster %q: %w", extID, opts.Cluster, err) + } + + return &Subnet{ + extID: subnetUUID, + }, nil + default: + return nil, fmt.Errorf("unknown response: %+v", subnets) + } +} + +func (n *networkingClient) getSubnetByExtID(subnetExtID uuid.UUID) (*Subnet, error) { + response, err := n.v4Client.SubnetsApiInstance.GetSubnetById( + utils.StringPtr(subnetExtID.String()), + ) + if err != nil { + return nil, fmt.Errorf( + "failed to find subnet with extID %q: %w", + subnetExtID, + err, + ) + } + subnet := response.GetData() + if subnet == nil { + return nil, fmt.Errorf("no subnet found with extID %q", subnetExtID) + } + + switch apiSubnet := subnet.(type) { + case *networkingapi.Subnet: + if apiSubnet.ExtId == nil { + return nil, fmt.Errorf("no extID found for subnet %q", subnetExtID) + } + subnetUUID, err := uuid.Parse(*apiSubnet.ExtId) + if err != nil { + return nil, + fmt.Errorf( + "failed to parse subnet extID %q for subnet %q: %w", + *apiSubnet.ExtId, + subnetExtID, + err, + ) + } + + return &Subnet{ + extID: subnetUUID, + }, nil + default: + return nil, fmt.Errorf("unknown response: %+v", subnet) + } +} diff --git a/adapter/prism.go b/adapter/prism.go new file mode 100644 index 00000000..d8724947 --- /dev/null +++ b/adapter/prism.go @@ -0,0 +1,65 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package adapter + +import ( + "context" + "fmt" + "time" + + "github.com/nutanix-cloud-native/prism-go-client/utils" + v4 "github.com/nutanix-cloud-native/prism-go-client/v4" + prismcommonapi "github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4/models/common/v1/config" + prismapi "github.com/nutanix/ntnx-api-golang-clients/prism-go-client/v4/models/prism/v4/config" + "k8s.io/apimachinery/pkg/util/wait" +) + +type PrismClient interface { + WaitForTaskCompletion(ctx context.Context, taskID string) ([]prismcommonapi.KVPair, error) +} + +func (c *client) Prism() PrismClient { + return &prismClient{v4Client: c.v4Client} +} + +type prismClient struct { + v4Client *v4.Client +} + +func (p *prismClient) WaitForTaskCompletion(ctx context.Context, taskID string) ([]prismcommonapi.KVPair, error) { + var data []prismcommonapi.KVPair + + if err := wait.PollUntilContextCancel( + ctx, + 100*time.Millisecond, + true, + func(ctx context.Context) (done bool, err error) { + task, err := p.v4Client.TasksApiInstance.GetTaskById(utils.StringPtr(taskID)) + if err != nil { + return false, fmt.Errorf("failed to get task %s: %w", taskID, err) + } + + taskData, ok := task.GetData().(prismapi.Task) + if !ok { + return false, fmt.Errorf("unexpected task data type %[1]T: %+[1]v", task.GetData()) + } + + if taskData.Status == nil { + return false, nil + } + + if *taskData.Status != prismapi.TASKSTATUS_SUCCEEDED { + return false, nil + } + + data = taskData.CompletionDetails + + return true, nil + }, + ); err != nil { + return nil, fmt.Errorf("failed to wait for task %s to complete: %w", taskID, err) + } + + return data, nil +}