Complete refactor to new grpc control plane and only http proxy for carts (#4)
Co-authored-by: matst80 <mats.tornberg@gmail.com> Reviewed-on: https://git.tornberg.me/mats/go-cart-actor/pulls/4 Co-authored-by: Mats Törnberg <mats@tornberg.me> Co-committed-by: Mats Törnberg <mats@tornberg.me>
This commit was merged in pull request #4.
This commit is contained in:
73
pkg/discovery/discovery.go
Normal file
73
pkg/discovery/discovery.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
toolsWatch "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
type K8sDiscovery struct {
|
||||
ctx context.Context
|
||||
client *kubernetes.Clientset
|
||||
}
|
||||
|
||||
func (k *K8sDiscovery) Discover() ([]string, error) {
|
||||
return k.DiscoverInNamespace("")
|
||||
}
|
||||
func (k *K8sDiscovery) DiscoverInNamespace(namespace string) ([]string, error) {
|
||||
pods, err := k.client.CoreV1().Pods(namespace).List(k.ctx, metav1.ListOptions{
|
||||
LabelSelector: "actor-pool=cart",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts := make([]string, 0, len(pods.Items))
|
||||
for _, pod := range pods.Items {
|
||||
hosts = append(hosts, pod.Status.PodIP)
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
type HostChange struct {
|
||||
Host string
|
||||
Type watch.EventType
|
||||
}
|
||||
|
||||
func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
||||
timeout := int64(30)
|
||||
watcherFn := func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return k.client.CoreV1().Pods("").Watch(k.ctx, metav1.ListOptions{
|
||||
LabelSelector: "actor-pool=cart",
|
||||
TimeoutSeconds: &timeout,
|
||||
})
|
||||
}
|
||||
watcher, err := toolsWatch.NewRetryWatcherWithContext(k.ctx, "1", &cache.ListWatch{WatchFunc: watcherFn})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch := make(chan HostChange)
|
||||
go func() {
|
||||
for event := range watcher.ResultChan() {
|
||||
|
||||
pod := event.Object.(*v1.Pod)
|
||||
// log.Printf("pod change %+v", pod.Status.Phase == v1.PodRunning)
|
||||
ch <- HostChange{
|
||||
Host: pod.Status.PodIP,
|
||||
Type: event.Type,
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func NewK8sDiscovery(client *kubernetes.Clientset) *K8sDiscovery {
|
||||
return &K8sDiscovery{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
102
pkg/discovery/discovery_mock.go
Normal file
102
pkg/discovery/discovery_mock.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
// MockDiscovery is an in-memory Discovery implementation for tests.
|
||||
// It allows deterministic injection of host additions/removals without
|
||||
// depending on Kubernetes API machinery.
|
||||
type MockDiscovery struct {
|
||||
mu sync.RWMutex
|
||||
hosts []string
|
||||
events chan HostChange
|
||||
closed bool
|
||||
started bool
|
||||
}
|
||||
|
||||
// NewMockDiscovery creates a mock discovery with an initial host list.
|
||||
func NewMockDiscovery(initial []string) *MockDiscovery {
|
||||
cp := make([]string, len(initial))
|
||||
copy(cp, initial)
|
||||
return &MockDiscovery{
|
||||
hosts: cp,
|
||||
events: make(chan HostChange, 32),
|
||||
}
|
||||
}
|
||||
|
||||
// Discover returns the current host snapshot.
|
||||
func (m *MockDiscovery) Discover() ([]string, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
cp := make([]string, len(m.hosts))
|
||||
copy(cp, m.hosts)
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
// Watch returns a channel that will receive HostChange events.
|
||||
// The channel is buffered; AddHost/RemoveHost push events non-blockingly.
|
||||
func (m *MockDiscovery) Watch() (<-chan HostChange, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.closed {
|
||||
return nil, context.Canceled
|
||||
}
|
||||
m.started = true
|
||||
return m.events, nil
|
||||
}
|
||||
|
||||
// AddHost inserts a new host (if absent) and emits an Added event.
|
||||
func (m *MockDiscovery) AddHost(host string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.closed {
|
||||
return
|
||||
}
|
||||
for _, h := range m.hosts {
|
||||
if h == host {
|
||||
return
|
||||
}
|
||||
}
|
||||
m.hosts = append(m.hosts, host)
|
||||
if m.started {
|
||||
m.events <- HostChange{Host: host, Type: watch.Added}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveHost removes a host (if present) and emits a Deleted event.
|
||||
func (m *MockDiscovery) RemoveHost(host string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.closed {
|
||||
return
|
||||
}
|
||||
idx := -1
|
||||
for i, h := range m.hosts {
|
||||
if h == host {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return
|
||||
}
|
||||
m.hosts = append(m.hosts[:idx], m.hosts[idx+1:]...)
|
||||
if m.started {
|
||||
m.events <- HostChange{Host: host, Type: watch.Deleted}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the event channel (idempotent).
|
||||
func (m *MockDiscovery) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.closed {
|
||||
return
|
||||
}
|
||||
m.closed = true
|
||||
close(m.events)
|
||||
}
|
||||
51
pkg/discovery/discovery_test.go
Normal file
51
pkg/discovery/discovery_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func TestDiscovery(t *testing.T) {
|
||||
config, err := clientcmd.BuildConfigFromFlags("", "/home/mats/.kube/config")
|
||||
if err != nil {
|
||||
t.Errorf("Error building config: %v", err)
|
||||
}
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Errorf("Error creating client: %v", err)
|
||||
}
|
||||
d := NewK8sDiscovery(client)
|
||||
res, err := d.DiscoverInNamespace("")
|
||||
if err != nil {
|
||||
t.Errorf("Error discovering: %v", err)
|
||||
}
|
||||
if len(res) == 0 {
|
||||
t.Errorf("Expected at least one host, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
config, err := clientcmd.BuildConfigFromFlags("", "/home/mats/.kube/config")
|
||||
if err != nil {
|
||||
t.Errorf("Error building config: %v", err)
|
||||
}
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Errorf("Error creating client: %v", err)
|
||||
}
|
||||
d := NewK8sDiscovery(client)
|
||||
ch, err := d.Watch()
|
||||
if err != nil {
|
||||
t.Errorf("Error watching: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case m := <-ch:
|
||||
t.Logf("Received watch %v", m)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("Timeout waiting for watch")
|
||||
}
|
||||
}
|
||||
6
pkg/discovery/types.go
Normal file
6
pkg/discovery/types.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package discovery
|
||||
|
||||
type Discovery interface {
|
||||
Discover() ([]string, error)
|
||||
Watch() (<-chan HostChange, error)
|
||||
}
|
||||
Reference in New Issue
Block a user