From d4d4ca236a0aba19cb096d73a84ab00dbf6d8017 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Wed, 11 Nov 2020 18:05:18 -0500 Subject: [PATCH] Start working on tests for auto discovery --- config.yaml | 1 - config/config_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++- k8s/client.go | 46 +++++++++++++++++++---- k8s/config.go | 5 ++- k8s/k8s.go | 14 ++----- k8stest/k8stest.go | 53 ++++++++++++++++++++++++++ metric/metric.go | 5 ++- 7 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 k8stest/k8stest.go diff --git a/config.yaml b/config.yaml index 78203cff..3689fb1d 100644 --- a/config.yaml +++ b/config.yaml @@ -19,7 +19,6 @@ kubernetes: cluster-mode: "out" auto-discover: true excluded-service-suffixes: - - primary - canary service-template: interval: 30s diff --git a/config/config_test.go b/config/config_test.go index 6262efef..d515e86a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,9 +2,13 @@ package config import ( "fmt" - "github.com/TwinProduction/gatus/core" + "strings" "testing" "time" + + "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/k8stest" + v1 "k8s.io/api/core/v1" ) 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) } } + +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) + } + } + } + +} diff --git a/k8s/client.go b/k8s/client.go index bcb5fed8..d2089a7f 100644 --- a/k8s/client.go +++ b/k8s/client.go @@ -6,28 +6,64 @@ import ( "os" "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/plugin/pkg/client/auth/gcp" "k8s.io/client-go/rest" "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 -func NewClient(clusterMode ClusterMode) (*kubernetes.Clientset, error) { +func NewClient(clusterMode ClusterMode) (KubernetesClientApi, error) { var kubeConfig *rest.Config var err error switch clusterMode { case ClusterModeIn: - kubeConfig, err = getInClusterConfig() + kubeConfig, err = rest.InClusterConfig() case ClusterModeOut: kubeConfig, err = getOutClusterConfig() + case ClusterModeMock: + return k8stest.GetMockedKubernetesClient(), nil default: return nil, fmt.Errorf("invalid cluster mode, try '%s' or '%s'", ClusterModeIn, ClusterModeOut) } if err != nil { 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 { @@ -47,7 +83,3 @@ func getOutClusterConfig() (*rest.Config, error) { flag.Parse() return clientcmd.BuildConfigFromFlags("", *kubeConfig) } - -func getInClusterConfig() (*rest.Config, error) { - return rest.InClusterConfig() -} diff --git a/k8s/config.go b/k8s/config.go index f5f8702b..45da6f01 100644 --- a/k8s/config.go +++ b/k8s/config.go @@ -39,6 +39,7 @@ type NamespaceConfig struct { type ClusterMode string const ( - ClusterModeIn ClusterMode = "in" - ClusterModeOut ClusterMode = "out" + ClusterModeIn ClusterMode = "in" + ClusterModeOut ClusterMode = "out" + ClusterModeMock ClusterMode = "mock" ) diff --git a/k8s/k8s.go b/k8s/k8s.go index 1b4e2ce0..8322a813 100644 --- a/k8s/k8s.go +++ b/k8s/k8s.go @@ -1,16 +1,10 @@ package k8s import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" + "k8s.io/api/core/v1" ) -// GetKubernetesServices return List of Services from given namespace -func GetKubernetesServices(client *kubernetes.Clientset, ns string) ([]corev1.Service, error) { - services, err := client.CoreV1().Services(ns).List(metav1.ListOptions{}) - if err != nil { - return nil, err - } - return services.Items, nil +// GetKubernetesServices return a list of Services from the given namespace +func GetKubernetesServices(client KubernetesClientApi, namespace string) ([]v1.Service, error) { + return client.GetServices(namespace) } diff --git a/k8stest/k8stest.go b/k8stest/k8stest.go new file mode 100644 index 00000000..4d8d0182 --- /dev/null +++ b/k8stest/k8stest.go @@ -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{}, + } +} diff --git a/metric/metric.go b/metric/metric.go index e8408a0e..e02a7310 100644 --- a/metric/metric.go +++ b/metric/metric.go @@ -2,12 +2,13 @@ package metric import ( "fmt" + "strconv" + "sync" + "github.com/TwinProduction/gatus/config" "github.com/TwinProduction/gatus/core" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "strconv" - "sync" ) var (