Start working on tests for auto discovery

This commit is contained in:
TwinProduction 2020-11-11 18:05:18 -05:00
parent dbc893fbc4
commit d4d4ca236a
7 changed files with 188 additions and 23 deletions

View File

@ -19,7 +19,6 @@ kubernetes:
cluster-mode: "out" cluster-mode: "out"
auto-discover: true auto-discover: true
excluded-service-suffixes: excluded-service-suffixes:
- primary
- canary - canary
service-template: service-template:
interval: 30s interval: 30s

View File

@ -2,9 +2,13 @@ package config
import ( import (
"fmt" "fmt"
"github.com/TwinProduction/gatus/core" "strings"
"testing" "testing"
"time" "time"
"github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/k8stest"
v1 "k8s.io/api/core/v1"
) )
func TestGetBeforeConfigIsLoaded(t *testing.T) { func TestGetBeforeConfigIsLoaded(t *testing.T) {
@ -311,3 +315,84 @@ services:
t.Errorf("config.Security.Basic.PasswordSha512Hash should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordSha512Hash) t.Errorf("config.Security.Basic.PasswordSha512Hash should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordSha512Hash)
} }
} }
func TestParseAndValidateConfigBytesWithNoServicesOrAutoDiscovery(t *testing.T) {
_, err := parseAndValidateConfigBytes([]byte(``))
if err != ErrNoServiceInConfig {
t.Error("The error returned should have been of type ErrNoServiceInConfig")
}
}
func TestParseAndValidateConfigBytesWithKubernetesAutoDiscovery(t *testing.T) {
var kubernetesServices []v1.Service
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-1", "default"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-2", "default"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-2-canary", "default"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-3", "kube-system"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-4", "tools"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-5", "tools"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-6", "tools"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-7", "metrics"))
kubernetesServices = append(kubernetesServices, k8stest.CreateTestServices("service-7-canary", "metrics"))
k8stest.InitializeMockedKubernetesClient(kubernetesServices)
config, err := parseAndValidateConfigBytes([]byte(`
debug: true
kubernetes:
cluster-mode: "mock"
auto-discover: true
excluded-service-suffixes:
- canary
service-template:
interval: 29s
conditions:
- "[STATUS] == 200"
namespaces:
- name: default
hostname-suffix: ".default.svc.cluster.local"
target-path: "/health"
- name: tools
hostname-suffix: ".tools.svc.cluster.local"
target-path: "/health"
excluded-services:
- service-6
- name: metrics
hostname-suffix: ".metrics.svc.cluster.local"
target-path: "/health"
`))
if err != nil {
t.Error("No error should've been returned")
}
if config == nil {
t.Fatal("Config shouldn't have been nil")
}
if config.Kubernetes == nil {
t.Fatal("Kuberbetes config shouldn't have been nil")
}
if len(config.Services) != 5 {
t.Error("Expected 5 services to have been added through k8s auto discovery, got", len(config.Services))
}
for _, service := range config.Services {
if service.Name == "service-2-canary" || service.Name == "service-7-canary" {
t.Errorf("service '%s' should've been excluded because excluded-service-suffixes has 'canary'", service.Name)
} else if service.Name == "service-6" {
t.Errorf("service '%s' should've been excluded because excluded-services has 'service-6'", service.Name)
} else if service.Name == "service-3" {
t.Errorf("service '%s' should've been excluded because the namespace 'kube-system' is not configured for auto discovery", service.Name)
} else {
if service.Interval != 29*time.Second {
t.Errorf("service '%s' should've had an interval of 29s, because the template is configured for it", service.Name)
}
if len(service.Conditions) != 1 {
t.Errorf("service '%s' should've had 1 condition", service.Name)
}
if len(service.Conditions) == 1 && *service.Conditions[0] != "[STATUS] == 200" {
t.Errorf("service '%s' should've had the condition '[STATUS] == 200', because the template is configured for it", service.Name)
}
if !strings.HasSuffix(service.URL, ".svc.cluster.local/health") {
t.Errorf("service '%s' should've had an URL with the suffix '.svc.cluster.local/health'", service.Name)
}
}
}
}

View File

@ -6,28 +6,64 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/TwinProduction/gatus/k8stest"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
) )
// KubernetesClientApi is a minimal interface for interacting with Kubernetes
// Created mostly to make mocking the Kubernetes client easier
type KubernetesClientApi interface {
GetServices(namespace string) ([]v1.Service, error)
}
// KubernetesClient is a working implementation of KubernetesClientApi
type KubernetesClient struct {
client *kubernetes.Clientset
}
// GetServices returns a list of services for a given namespace
func (k *KubernetesClient) GetServices(namespace string) ([]v1.Service, error) {
services, err := k.client.CoreV1().Services(namespace).List(metav1.ListOptions{})
if err != nil {
return nil, err
}
return services.Items, nil
}
// NewKubernetesClient creates a KubernetesClient
func NewKubernetesClient(client *kubernetes.Clientset) *KubernetesClient {
return &KubernetesClient{
client: client,
}
}
// NewClient creates a Kubernetes client for the given ClusterMode // NewClient creates a Kubernetes client for the given ClusterMode
func NewClient(clusterMode ClusterMode) (*kubernetes.Clientset, error) { func NewClient(clusterMode ClusterMode) (KubernetesClientApi, error) {
var kubeConfig *rest.Config var kubeConfig *rest.Config
var err error var err error
switch clusterMode { switch clusterMode {
case ClusterModeIn: case ClusterModeIn:
kubeConfig, err = getInClusterConfig() kubeConfig, err = rest.InClusterConfig()
case ClusterModeOut: case ClusterModeOut:
kubeConfig, err = getOutClusterConfig() kubeConfig, err = getOutClusterConfig()
case ClusterModeMock:
return k8stest.GetMockedKubernetesClient(), nil
default: default:
return nil, fmt.Errorf("invalid cluster mode, try '%s' or '%s'", ClusterModeIn, ClusterModeOut) return nil, fmt.Errorf("invalid cluster mode, try '%s' or '%s'", ClusterModeIn, ClusterModeOut)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get cluster config for mode '%s': %s", clusterMode, err.Error()) return nil, fmt.Errorf("unable to get cluster config for mode '%s': %s", clusterMode, err.Error())
} }
return kubernetes.NewForConfig(kubeConfig) client, err := kubernetes.NewForConfig(kubeConfig)
if err != nil {
return nil, err
}
return NewKubernetesClient(client), nil
} }
func homeDir() string { func homeDir() string {
@ -47,7 +83,3 @@ func getOutClusterConfig() (*rest.Config, error) {
flag.Parse() flag.Parse()
return clientcmd.BuildConfigFromFlags("", *kubeConfig) return clientcmd.BuildConfigFromFlags("", *kubeConfig)
} }
func getInClusterConfig() (*rest.Config, error) {
return rest.InClusterConfig()
}

View File

@ -39,6 +39,7 @@ type NamespaceConfig struct {
type ClusterMode string type ClusterMode string
const ( const (
ClusterModeIn ClusterMode = "in" ClusterModeIn ClusterMode = "in"
ClusterModeOut ClusterMode = "out" ClusterModeOut ClusterMode = "out"
ClusterModeMock ClusterMode = "mock"
) )

View File

@ -1,16 +1,10 @@
package k8s package k8s
import ( import (
corev1 "k8s.io/api/core/v1" "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
) )
// GetKubernetesServices return List of Services from given namespace // GetKubernetesServices return a list of Services from the given namespace
func GetKubernetesServices(client *kubernetes.Clientset, ns string) ([]corev1.Service, error) { func GetKubernetesServices(client KubernetesClientApi, namespace string) ([]v1.Service, error) {
services, err := client.CoreV1().Services(ns).List(metav1.ListOptions{}) return client.GetServices(namespace)
if err != nil {
return nil, err
}
return services.Items, nil
} }

53
k8stest/k8stest.go Normal file
View File

@ -0,0 +1,53 @@
package k8stest
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
mockedKubernetesClient *MockKubernetesClient
)
// MockKubernetesClient is a mocked implementation of k8s.KubernetesClientApi
type MockKubernetesClient struct {
Services []v1.Service
}
// GetServices returns a list of services in a given namespace
func (mock *MockKubernetesClient) GetServices(namespace string) ([]v1.Service, error) {
var services []v1.Service
for _, service := range mock.Services {
if service.Namespace == namespace {
services = append(services, service)
}
}
return services, nil
}
// GetMockedKubernetesClient returns a mocked implementation of k8s.KubernetesClientApi
func GetMockedKubernetesClient() *MockKubernetesClient {
if mockedKubernetesClient != nil {
return mockedKubernetesClient
}
InitializeMockedKubernetesClient(nil)
return mockedKubernetesClient
}
// InitializeMockedKubernetesClient initializes a MockKubernetesClient with a given list of services
func InitializeMockedKubernetesClient(services []v1.Service) {
mockedKubernetesClient = &MockKubernetesClient{
Services: services,
}
}
// CreateTestServices creates a mocked service for testing purposes
func CreateTestServices(name, namespace string) v1.Service {
return v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: v1.ServiceSpec{},
}
}

View File

@ -2,12 +2,13 @@ package metric
import ( import (
"fmt" "fmt"
"strconv"
"sync"
"github.com/TwinProduction/gatus/config" "github.com/TwinProduction/gatus/config"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"strconv"
"sync"
) )
var ( var (