Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: exchange Metal API keys for LB API OAuth2 tokens #3

Merged
merged 6 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/lbaas/v1/configuration.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion metal/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func newLoadBalancers(client *packngo.Client, k8sclient kubernetes.Interface, pr
impl = empty.NewLB(k8sclient, lbconfig)
case "emlb":
klog.Info("loadbalancer implementation enabled: emlb")
impl = emlb.NewLB(k8sclient, lbconfig)
impl = emlb.NewLB(k8sclient, lbconfig, client.APIKey, projectID)
default:
klog.Info("loadbalancer implementation disabled")
impl = nil
Expand Down
102 changes: 102 additions & 0 deletions metal/loadbalancers/emlb/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package emlb

import (
"context"
"fmt"
"reflect"

lbaas "github.com/equinix/cloud-provider-equinix-metal/internal/lbaas/v1"
)

const ProviderID = "loadpvd-gOB_-byp5ebFo7A3LHv2B"

var LBMetros = map[string]string{
"da": "lctnloc--uxs0GLeAELHKV8GxO_AI",
"ny": "lctnloc-Vy-1Qpw31mPi6RJQwVf9A",
"sv": "lctnloc-H5rl2M2VL5dcFmdxhbEKx",
}

type controller struct {
client *lbaas.APIClient
tokenExchanger *MetalTokenExchanger
projectID string
metro string
}

func NewController(metalAPIKey, projectID, metro string) *controller {
controller := &controller{}
emlbConfig := lbaas.NewConfiguration()

controller.client = lbaas.NewAPIClient(emlbConfig)
controller.tokenExchanger = &MetalTokenExchanger{
metalAPIKey: metalAPIKey,
client: controller.client.GetConfig().HTTPClient,
}
controller.projectID = projectID
controller.metro = metro

return controller
}

func (c *controller) createLoadBalancer(ctx context.Context, config map[string]string) (map[string]string, error) {
outputProperties := map[string]string{}

ctx = context.WithValue(ctx, lbaas.ContextOAuth2, c.tokenExchanger)

metro := config["metro"]

locationId, ok := LBMetros[metro]
if !ok {
return nil, fmt.Errorf("could not determine load balancer location for metro %v; valid values are %v", metro, reflect.ValueOf(LBMetros).Keys())
}
lbCreateRequest := lbaas.LoadBalancerCreate{
Name: "", // TODO generate from service definition. Maybe "svcNamespace:svcName"? Do we need to know the cluster name here?
LocationId: locationId,
ProviderId: ProviderID,
}

// TODO lb, resp, err :=
_, _, err := c.client.ProjectsApi.CreateLoadBalancer(ctx, "TODO: project ID").LoadBalancerCreate(lbCreateRequest).Execute()
if err != nil {
return nil, err
}

// TODO create other resources
return outputProperties, nil
}

func (c *controller) updateLoadBalancer(ctx context.Context, id string, config map[string]string) (map[string]string, error) {
outputProperties := map[string]string{}

ctx = context.WithValue(ctx, lbaas.ContextOAuth2, c.tokenExchanger)

// TODO delete other resources

// TODO lb, resp, err :=
_, err := c.client.LoadBalancersApi.DeleteLoadBalancer(ctx, id).Execute()
if err != nil {
return nil, err
}

return outputProperties, nil
}

func (c *controller) deleteLoadBalancer(ctx context.Context, id string, config map[string]string) (map[string]string, error) {
outputProperties := map[string]string{}

tokenExchanger := &MetalTokenExchanger{
metalAPIKey: "TODO",
client: c.client.GetConfig().HTTPClient,
}
ctx = context.WithValue(ctx, lbaas.ContextOAuth2, tokenExchanger)

// TODO delete other resources

// TODO lb, resp, err :=
_, err := c.client.LoadBalancersApi.DeleteLoadBalancer(ctx, id).Execute()
if err != nil {
return nil, err
}

return outputProperties, nil
}
71 changes: 63 additions & 8 deletions metal/loadbalancers/emlb/emlb.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,92 @@ package emlb
import (
"context"

lbaas "github.com/equinix/cloud-provider-equinix-metal/internal/lbaas/v1"
"github.com/equinix/cloud-provider-equinix-metal/metal/loadbalancers"
"k8s.io/client-go/kubernetes"
)

type LB struct {
client *lbaas.APIClient
loadBalancerLocation *lbaas.LoadBalancerLocation
controller *controller
}

func NewLB(k8sclient kubernetes.Interface, config string) *LB {
func NewLB(k8sclient kubernetes.Interface, config, metalAPIKey, projectID string) *LB {
// Parse config for Equinix Metal Load Balancer
// An example config using Dallas as the location would look like
// The format is emlb://<location>
// An example config using Dallas as the location would look like emlb://da
// it may have an extra slash at the beginning or end, so get rid of it
metro := config

lb := &LB{}
emlbConfig := lbaas.NewConfiguration()
lb.client = lbaas.NewAPIClient(emlbConfig)
lb.loadBalancerLocation.Id = &config
lb.controller = NewController(metalAPIKey, projectID, metro)

return lb
}

func (l *LB) AddService(ctx context.Context, svcNamespace, svcName, ip string, nodes []loadbalancers.Node) error {
ctreatma marked this conversation as resolved.
Show resolved Hide resolved
/*
1. Gather the properties we need: Metal API key, port number(s), cluster name(?), target IP(s?)
What we need here is:
- The NodePort (for first pass we will use the same port on the LB, so if NodePort is 8000 we use 8000 on LB)
- The public IPs of the nodes on which the service is running
*/
additionalProperties := map[string]string{}

// 2. Create the infrastructure (what do we need to return here? lb name and/or ID? anything else?)
cprivitere marked this conversation as resolved.
Show resolved Hide resolved
_, err := l.controller.createLoadBalancer(ctx, additionalProperties)

if err != nil {
return err
}

/*
3. Add the annotations
- ID of the load balancer
- Name of the load balancer
- Metro of the load balancer
- IP address of the load balancer
- Listener port that this service is using
*/
return nil
}

func (l *LB) RemoveService(ctx context.Context, svcNamespace, svcName, ip string) error {
// 1. Gather the properties we need: ID of load balancer
loadBalancerId := "TODO"
additionalProperties := map[string]string{}

// 2. Delete the infrastructure (do we need to return anything here?)
cprivitere marked this conversation as resolved.
Show resolved Hide resolved
_, err := l.controller.deleteLoadBalancer(ctx, loadBalancerId, additionalProperties)

if err != nil {
return err
}

// 3. No need to remove the annotations because the annotated object was deleted

return nil
}

func (l *LB) UpdateService(ctx context.Context, svcNamespace, svcName string, nodes []loadbalancers.Node) error {
/*
1. Gather the properties we need:
- load balancer ID
- NodePort
- Public IP addresses of the nodes on which the target pods are running
*/
loadBalancerId := "TODO"
additionalProperties := map[string]string{}

// 2. Update infrastructure change (do we need to return anything here? or are all changes reflected by properties from [1]?)
_, err := l.controller.updateLoadBalancer(ctx, loadBalancerId, additionalProperties)

if err != nil {
return err
}

/*
3. Update the annotations
- Listener port that this service is using
*/

return nil
}
57 changes: 57 additions & 0 deletions metal/loadbalancers/emlb/metal_token_exchanger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package emlb

import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"

"golang.org/x/oauth2"
)

type MetalTokenExchanger struct {
metalAPIKey string
client *http.Client
}

func (m *MetalTokenExchanger) Token() (*oauth2.Token, error) {
tokenExchangeURL := "https://iam.metalctrl.io/api-keys/exchange"
tokenExchangeRequest, err := http.NewRequest("POST", tokenExchangeURL, nil)
if err != nil {
return nil, err
}
tokenExchangeRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %v", m.metalAPIKey))

resp, err := m.client.Do(tokenExchangeRequest)
if err != nil {
return nil, err
}

body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token exchange request failed with status %v, body %v", resp.StatusCode, body)
}

token := oauth2.Token{}
err = json.Unmarshal(body, &token)
if err != nil {
fmt.Println(len(body))
fmt.Println(token)
fmt.Println(err)
return nil, err
}

expiresIn := token.Extra("expires_in")
if expiresIn != nil {
expiresInSeconds := expiresIn.(int)
token.Expiry = time.Now().Add(time.Second * time.Duration(expiresInSeconds))
}

return &token, nil
}