mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-21 14:41:12 +02:00
This PR contains generic simple TCP loadbalancer code, and glue code for firecracker provisioner to use this loadbalancer. K8s control plane is passed through the load balancer, and Talos API is passed only to the init node (for now, as some APIs, including kubeconfig, don't work with non-init node). Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package upstream_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/talos-systems/talos/internal/pkg/loadbalancer/upstream"
|
|
"github.com/talos-systems/talos/pkg/retry"
|
|
)
|
|
|
|
type mockBackend string
|
|
|
|
func (b mockBackend) HealthCheck(ctx context.Context) error {
|
|
switch string(b) {
|
|
case "fail":
|
|
return errors.New("fail")
|
|
case "success":
|
|
return nil
|
|
default:
|
|
<-ctx.Done()
|
|
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
type ListSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func (suite *ListSuite) TestEmpty() {
|
|
l, err := upstream.NewList(nil)
|
|
suite.Require().NoError(err)
|
|
|
|
defer l.Shutdown()
|
|
|
|
backend, err := l.Pick()
|
|
suite.Assert().Nil(backend)
|
|
suite.Assert().EqualError(err, "no upstreams available")
|
|
}
|
|
|
|
func (suite *ListSuite) TestRoundRobin() {
|
|
l, err := upstream.NewList([]upstream.Backend{mockBackend("one"), mockBackend("two"), mockBackend("three")})
|
|
suite.Require().NoError(err)
|
|
|
|
defer l.Shutdown()
|
|
|
|
backend, err := l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("two"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("three"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
}
|
|
|
|
func (suite *ListSuite) TestDownUp() {
|
|
l, err := upstream.NewList(
|
|
[]upstream.Backend{
|
|
mockBackend("one"),
|
|
mockBackend("two"),
|
|
mockBackend("three"),
|
|
},
|
|
upstream.WithLowHighScores(-3, 3),
|
|
upstream.WithInitialScore(1),
|
|
upstream.WithScoreDeltas(-1, 1),
|
|
upstream.WithHealthcheckInterval(time.Hour),
|
|
)
|
|
suite.Require().NoError(err)
|
|
|
|
defer l.Shutdown()
|
|
|
|
backend, err := l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
l.Down(mockBackend("two")) // score == 0
|
|
l.Down(mockBackend("two")) // score == -1
|
|
l.Down(mockBackend("three")) // score == 0
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("three"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("three"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
l.Down(mockBackend("three")) // score == -1
|
|
l.Up(mockBackend("two")) // score == 0
|
|
l.Up(mockBackend("two")) // score == 1
|
|
l.Up(mockBackend("two")) // score == 2
|
|
l.Up(mockBackend("two")) // score == 3
|
|
l.Up(mockBackend("two")) // score == 3 (capped at highScore)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("two"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
l.Down(mockBackend("two")) // score == 2
|
|
l.Down(mockBackend("two")) // score == 1
|
|
l.Down(mockBackend("two")) // score == 0
|
|
l.Down(mockBackend("two")) // score == -1
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
l.Down(mockBackend("two")) // score == -2
|
|
l.Down(mockBackend("two")) // score == -3
|
|
l.Down(mockBackend("two")) // score == -3 (capped at lowScore)
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("one"), backend)
|
|
suite.Assert().NoError(err)
|
|
|
|
l.Up(mockBackend("two")) // score == -2
|
|
l.Up(mockBackend("two")) // score == -1
|
|
l.Up(mockBackend("two")) // score == 0
|
|
|
|
backend, err = l.Pick()
|
|
suite.Assert().Equal(mockBackend("two"), backend)
|
|
suite.Assert().NoError(err)
|
|
}
|
|
|
|
func (suite *ListSuite) TestHealthcheck() {
|
|
l, err := upstream.NewList(
|
|
[]upstream.Backend{
|
|
mockBackend("success"),
|
|
mockBackend("fail"),
|
|
mockBackend("timeout"),
|
|
},
|
|
upstream.WithLowHighScores(-1, 1),
|
|
upstream.WithInitialScore(1),
|
|
upstream.WithScoreDeltas(-1, 1),
|
|
upstream.WithHealthcheckInterval(10*time.Millisecond),
|
|
upstream.WithHealthcheckTimeout(time.Millisecond),
|
|
)
|
|
suite.Require().NoError(err)
|
|
|
|
defer l.Shutdown()
|
|
|
|
time.Sleep(20 * time.Millisecond) // let healthchecks run
|
|
|
|
// when health info converges, "success" should be the only backend left
|
|
suite.Require().NoError(retry.Constant(time.Second, retry.WithUnits(time.Millisecond)).Retry(func() error {
|
|
for i := 0; i < 10; i++ {
|
|
backend, err := l.Pick()
|
|
if err != nil {
|
|
return retry.UnexpectedError(err)
|
|
}
|
|
|
|
if backend.(mockBackend) != mockBackend("success") {
|
|
return retry.ExpectedError(fmt.Errorf("unexpected %v", backend))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
}
|
|
|
|
func TestListSuite(t *testing.T) {
|
|
suite.Run(t, new(ListSuite))
|
|
}
|