Skip to content
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
21 changes: 21 additions & 0 deletions cloud/linode/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import (
"context"
"errors"
"fmt"
"net/http"
"os"
Expand Down Expand Up @@ -52,6 +53,8 @@
DeleteFirewall(ctx context.Context, fwid int) error
GetFirewall(context.Context, int) (*linodego.Firewall, error)
UpdateFirewallRules(context.Context, int, linodego.FirewallRuleSet) (*linodego.FirewallRuleSet, error)

GetProfile(ctx context.Context) (*linodego.Profile, error)
}

// linodego.Client implements Client
Expand All @@ -73,3 +76,21 @@
klog.V(3).Infof("Linode client created with default timeout of %v", timeout)
return client, nil
}

func CheckClientAuthenticated(ctx context.Context, client Client) (bool, error) {
_, err := client.GetProfile(ctx)
if err == nil {
return true, nil
}

var linodeErr *linodego.Error
if !errors.As(err, &linodeErr) {
return false, err
}

Check warning on line 89 in cloud/linode/client/client.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/client/client.go#L88-L89

Added lines #L88 - L89 were not covered by tests

if linodego.ErrHasStatus(err, http.StatusUnauthorized) {
return false, nil
}

return false, err
}
15 changes: 15 additions & 0 deletions cloud/linode/client/mocks/mock_client.go

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

60 changes: 44 additions & 16 deletions cloud/linode/cloud.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package linode

import (
"context"
"fmt"
"io"
"net"
Expand All @@ -19,11 +20,12 @@

const (
// The name of this cloudprovider
ProviderName = "linode"
accessTokenEnv = "LINODE_API_TOKEN"
regionEnv = "LINODE_REGION"
ciliumLBType = "cilium-bgp"
nodeBalancerLBType = "nodebalancer"
ProviderName = "linode"
accessTokenEnv = "LINODE_API_TOKEN"
regionEnv = "LINODE_REGION"
ciliumLBType = "cilium-bgp"
nodeBalancerLBType = "nodebalancer"
tokenHealthCheckPeriod = 5 * time.Minute
)

var supportedLoadBalancerTypes = []string{ciliumLBType, nodeBalancerLBType}
Expand All @@ -32,9 +34,10 @@
// We expect it to be initialized with flags external to this package, likely in
// main.go
var Options struct {
KubeconfigFlag *pflag.Flag
LinodeGoDebug bool
EnableRouteController bool
KubeconfigFlag *pflag.Flag
LinodeGoDebug bool
EnableRouteController bool
EnableTokenHealthChecker bool
// Deprecated: use VPCNames instead
VPCName string
VPCNames string
Expand All @@ -43,13 +46,15 @@
IpHolderSuffix string
LinodeExternalNetwork *net.IPNet
NodeBalancerTags []string
GlobalStopChannel chan<- struct{}
}

type linodeCloud struct {
client client.Client
instances cloudprovider.InstancesV2
loadbalancers cloudprovider.LoadBalancer
routes cloudprovider.Routes
client client.Client
instances cloudprovider.InstancesV2
loadbalancers cloudprovider.LoadBalancer
routes cloudprovider.Routes
linodeTokenHealthChecker *healthChecker
}

var instanceCache *instances
Expand Down Expand Up @@ -91,6 +96,24 @@
linodeClient.SetDebug(true)
}

var healthChecker *healthChecker

if Options.EnableTokenHealthChecker {
authenticated, err := client.CheckClientAuthenticated(context.TODO(), linodeClient)
if err != nil {
return nil, fmt.Errorf("linode client authenticated connection error: %w", err)
}

Check warning on line 105 in cloud/linode/cloud.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/cloud.go#L102-L105

Added lines #L102 - L105 were not covered by tests

if !authenticated {
return nil, fmt.Errorf("linode api token %q is invalid", accessTokenEnv)
}

Check warning on line 109 in cloud/linode/cloud.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/cloud.go#L107-L109

Added lines #L107 - L109 were not covered by tests

healthChecker, err = newHealthChecker(apiToken, timeout, tokenHealthCheckPeriod, Options.GlobalStopChannel)
if err != nil {
return nil, fmt.Errorf("unable to initialize healthchecker: %w", err)
}

Check warning on line 114 in cloud/linode/cloud.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/cloud.go#L111-L114

Added lines #L111 - L114 were not covered by tests
}

if Options.VPCName != "" && Options.VPCNames != "" {
return nil, fmt.Errorf("cannot have both vpc-name and vpc-names set")
}
Expand Down Expand Up @@ -126,10 +149,11 @@

// create struct that satisfies cloudprovider.Interface
lcloud := &linodeCloud{
client: linodeClient,
instances: instanceCache,
loadbalancers: newLoadbalancers(linodeClient, region),
routes: routes,
client: linodeClient,
instances: instanceCache,
loadbalancers: newLoadbalancers(linodeClient, region),
routes: routes,
linodeTokenHealthChecker: healthChecker,
}
return lcloud, nil
}
Expand All @@ -140,6 +164,10 @@
serviceInformer := sharedInformer.Core().V1().Services()
nodeInformer := sharedInformer.Core().V1().Nodes()

if c.linodeTokenHealthChecker != nil {
go c.linodeTokenHealthChecker.Run(stopCh)
}

Check warning on line 169 in cloud/linode/cloud.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/cloud.go#L167-L169

Added lines #L167 - L169 were not covered by tests

serviceController := newServiceController(c.loadbalancers.(*loadbalancers), serviceInformer)
go serviceController.Run(stopCh)

Expand Down
63 changes: 63 additions & 0 deletions cloud/linode/health_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package linode

import (
"context"
"time"

"github.com/linode/linode-cloud-controller-manager/cloud/linode/client"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/klog/v2"
)

type healthChecker struct {
period time.Duration
linodeClient client.Client
stopCh chan<- struct{}
}

func newHealthChecker(apiToken string, timeout time.Duration, period time.Duration, stopCh chan<- struct{}) (*healthChecker, error) {
client, err := client.New(apiToken, timeout)
if err != nil {
return nil, err
}

Check warning on line 22 in cloud/linode/health_check.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/health_check.go#L21-L22

Added lines #L21 - L22 were not covered by tests

return &healthChecker{
period: period,
linodeClient: client,
stopCh: stopCh,
}, nil
}

func (r *healthChecker) Run(stopCh <-chan struct{}) {
ctx := wait.ContextForChannel(stopCh)
wait.Until(r.worker(ctx), r.period, stopCh)
}

func (r *healthChecker) worker(ctx context.Context) func() {
return func() {
r.do(ctx)
}
}

func (r *healthChecker) do(ctx context.Context) {
if r.stopCh == nil {
klog.Errorf("stop signal already fired. nothing to do")
return
}

Check warning on line 46 in cloud/linode/health_check.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/health_check.go#L44-L46

Added lines #L44 - L46 were not covered by tests

authenticated, err := client.CheckClientAuthenticated(ctx, r.linodeClient)
if err != nil {
klog.Warningf("unable to determine linode client authentication status: %s", err.Error())
return
}

if !authenticated {
klog.Error("detected invalid linode api token: stopping controllers")

close(r.stopCh)
r.stopCh = nil
return
}

klog.Info("linode api token is healthy")
}
153 changes: 153 additions & 0 deletions cloud/linode/health_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package linode

import (
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/linode/linode-cloud-controller-manager/cloud/linode/client/mocks"
"github.com/linode/linodego"
)

func TestHealthCheck(t *testing.T) {
testCases := []struct {
name string
f func(*testing.T, *mocks.MockClient)
}{
{
name: "Test succeeding calls to linode api stop signal is not fired",
f: testSucceedingCallsToLinodeAPIHappenStopSignalNotFired,
},
{
name: "Test Unauthorized calls to linode api stop signal is fired",
f: testFailingCallsToLinodeAPIHappenStopSignalFired,
},
{
name: "Test failing calls to linode api stop signal is not fired",
f: testErrorCallsToLinodeAPIHappenStopSignalNotFired,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

client := mocks.NewMockClient(ctrl)
tc.f(t, client)
})
}
}

func testSucceedingCallsToLinodeAPIHappenStopSignalNotFired(t *testing.T, client *mocks.MockClient) {
writableStopCh := make(chan struct{})
readableStopCh := make(chan struct{})

client.EXPECT().GetProfile(gomock.Any()).Times(2).Return(&linodego.Profile{}, nil)

hc, err := newHealthChecker("validToken", 1*time.Second, 1*time.Second, writableStopCh)
if err != nil {
t.Fatalf("expected a nil error, got %v", err)
}
// inject mocked linodego.Client
hc.linodeClient = client

defer close(readableStopCh)
go hc.Run(readableStopCh)

// wait for two checks to happen
time.Sleep(1500 * time.Millisecond)

select {
case <-writableStopCh:
t.Error("healthChecker sent stop signal")
default:
}
}

func testFailingCallsToLinodeAPIHappenStopSignalFired(t *testing.T, client *mocks.MockClient) {
writableStopCh := make(chan struct{})
readableStopCh := make(chan struct{})

client.EXPECT().GetProfile(gomock.Any()).Times(1).Return(&linodego.Profile{}, nil)

hc, err := newHealthChecker("validToken", 1*time.Second, 1*time.Second, writableStopCh)
if err != nil {
t.Fatalf("expected a nil error, got %v", err)
}
// inject mocked linodego.Client
hc.linodeClient = client

defer close(readableStopCh)
go hc.Run(readableStopCh)

// wait for check to happen
time.Sleep(500 * time.Millisecond)

select {
case <-writableStopCh:
t.Error("healthChecker sent stop signal")
default:
}

// invalidate token
client.EXPECT().GetProfile(gomock.Any()).Times(1).Return(&linodego.Profile{}, &linodego.Error{Code: 401, Message: "Invalid Token"})

// wait for check to happen
time.Sleep(1 * time.Second)

select {
case <-writableStopCh:
default:
t.Error("healthChecker did not send stop signal")
}
}

func testErrorCallsToLinodeAPIHappenStopSignalNotFired(t *testing.T, client *mocks.MockClient) {
writableStopCh := make(chan struct{})
readableStopCh := make(chan struct{})

client.EXPECT().GetProfile(gomock.Any()).Times(1).Return(&linodego.Profile{}, nil)

hc, err := newHealthChecker("validToken", 1*time.Second, 1*time.Second, writableStopCh)
if err != nil {
t.Fatalf("expected a nil error, got %v", err)
}
// inject mocked linodego.Client
hc.linodeClient = client

defer close(readableStopCh)
go hc.Run(readableStopCh)

// wait for check to happen
time.Sleep(500 * time.Millisecond)

select {
case <-writableStopCh:
t.Error("healthChecker sent stop signal")
default:
}

// simulate server error
client.EXPECT().GetProfile(gomock.Any()).Times(1).Return(&linodego.Profile{}, &linodego.Error{Code: 500})

// wait for check to happen
time.Sleep(1 * time.Second)

select {
case <-writableStopCh:
t.Error("healthChecker sent stop signal")
default:
}

client.EXPECT().GetProfile(gomock.Any()).Times(1).Return(&linodego.Profile{}, nil)

// wait for check to happen
time.Sleep(1 * time.Second)

select {
case <-writableStopCh:
t.Error("healthChecker sent stop signal")
default:
}
}
Loading
Loading