diff --git a/Jenkinsfile b/Jenkinsfile index 9b70391c..e47e46a0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -49,7 +49,7 @@ toolsNode(toolsImage: 'stakater/pipeline-tools:1.5.1') { stage('Build Binary') { sh """ cd ${srcDir} - go build -o ..out/${repoName.toLowerCase()} + go build -o ../out/${repoName.toLowerCase()} """ } diff --git a/README.md b/README.md index f32d1798..f69d8b92 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This controller will continuously watch ingresses in the namespace it is running Currently we support the following monitors: - [UptimeRobot](https://uptimerobot.com) +- [Pingdom](https://pingdom.com) ([Additional Config](https://github.com/stakater/IngressMonitorController/blob/master/docs/pingdom-configuration.md)) ## Deploying to Kubernetes @@ -83,7 +84,7 @@ type MonitorService interface { Update(m Monitor) GetByName(name string) (*Monitor, error) Remove(m Monitor) - Setup(apiKey string, url string, alertContacts string) + Setup(provider Provider) } ``` diff --git a/docs/pingdom-configuration.md b/docs/pingdom-configuration.md new file mode 100644 index 00000000..f2788f8e --- /dev/null +++ b/docs/pingdom-configuration.md @@ -0,0 +1,10 @@ +# Pingdom Configuration + +Currently additional pingdom configurations can be added through a set of annotations to each ingress object, the current supported annotations are: + +| Annotation | Description | +|:--------------------------------------------------------:|:------------------------------------------------:| +| monitor.stakater.com/pingdom/resolution | The pingdom check interval in minutes | +| monitor.stakater.com/pingdom/send-notification-when-down | How many failed check attempts before notifying | +| monitor.stakater.com/pingdom/paused | Set to "true" to pause checks | +| monitor.stakater.com/pingdom/notify-when-back-up | Set to "false" to disable recovery notifications | \ No newline at end of file diff --git a/src/..out/ingressmonitorcontroller b/src/..out/ingressmonitorcontroller deleted file mode 100755 index 71e464c1..00000000 Binary files a/src/..out/ingressmonitorcontroller and /dev/null differ diff --git a/src/config.go b/src/config.go index ce8319ec..a3877026 100644 --- a/src/config.go +++ b/src/config.go @@ -18,17 +18,20 @@ type Provider struct { ApiKey string `yaml:"apiKey"` ApiURL string `yaml:"apiURL"` AlertContacts string `yaml:"alertContacts"` + Username string `yaml:"username"` + Password string `yaml:"password"` } func (p *Provider) createMonitorService() MonitorServiceProxy { monitorService := (&MonitorServiceProxy{}).OfType(p.Name) - monitorService.Setup(p.ApiKey, p.ApiURL, p.AlertContacts) + monitorService.Setup(*p) return monitorService } func ReadConfig(filePath string) Config { var config Config // Read YML + log.Println("Reading YAML Configuration") source, err := ioutil.ReadFile(filePath) if err != nil { log.Panic(err) diff --git a/src/controller.go b/src/controller.go index 493edacd..6b6845b7 100644 --- a/src/controller.go +++ b/src/controller.go @@ -20,7 +20,7 @@ const monitorHealthAnnotation = "monitor.stakater.com/healthEndpoint" // "/healt // MonitorController which can be used for monitoring ingresses type MonitorController struct { - clientset *kubernetes.Clientset + kubeClient kubernetes.Interface namespace string indexer cache.Indexer queue workqueue.RateLimitingInterface @@ -29,11 +29,11 @@ type MonitorController struct { config Config } -func NewMonitorController(namespace string, clientset *kubernetes.Clientset, config Config) *MonitorController { +func NewMonitorController(namespace string, kubeClient kubernetes.Interface, config Config) *MonitorController { controller := &MonitorController{ - clientset: clientset, - namespace: namespace, - config: config, + kubeClient: kubeClient, + namespace: namespace, + config: config, } controller.monitorServices = setupMonitorServicesForProviders(config.Providers) @@ -41,7 +41,7 @@ func NewMonitorController(namespace string, clientset *kubernetes.Clientset, con queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) // Create the Ingress Watcher - ingressListWatcher := cache.NewListWatchFromClient(clientset.ExtensionsV1beta1().RESTClient(), "ingresses", namespace, fields.Everything()) + ingressListWatcher := cache.NewListWatchFromClient(kubeClient.ExtensionsV1beta1().RESTClient(), "ingresses", namespace, fields.Everything()) indexer, informer := cache.NewIndexerInformer(ingressListWatcher, &v1beta1.Ingress{}, 0, cache.ResourceEventHandlerFuncs{ AddFunc: controller.onIngressAdded, @@ -154,9 +154,9 @@ func (c *MonitorController) getMonitorName(ingressName string, namespace string) func (c *MonitorController) getMonitorURL(ingress *v1beta1.Ingress) string { ingressWrapper := IngressWrapper{ - ingress: ingress, - namespace: c.namespace, - clientset: c.clientset, + ingress: ingress, + namespace: c.namespace, + kubeClient: c.kubeClient, } return ingressWrapper.getURL() @@ -174,7 +174,7 @@ func (c *MonitorController) handleIngressOnCreationOrUpdation(ingress *v1beta1.I if value, ok := annotations[monitorEnabledAnnotation]; ok { if value == "true" { // Annotation exists and is enabled - c.createOrUpdateMonitors(monitorName, monitorURL) + c.createOrUpdateMonitors(monitorName, monitorURL, annotations) } else { // Annotation exists but is disabled c.removeMonitorsIfExist(monitorName) @@ -202,14 +202,14 @@ func (c *MonitorController) removeMonitorIfExists(monitorService MonitorServiceP } } -func (c *MonitorController) createOrUpdateMonitors(monitorName string, monitorURL string) { +func (c *MonitorController) createOrUpdateMonitors(monitorName string, monitorURL string, annotations map[string]string) { for index := 0; index < len(c.monitorServices); index++ { monitorService := c.monitorServices[index] - c.createOrUpdateMonitor(monitorService, monitorName, monitorURL) + c.createOrUpdateMonitor(monitorService, monitorName, monitorURL, annotations) } } -func (c *MonitorController) createOrUpdateMonitor(monitorService MonitorServiceProxy, monitorName string, monitorURL string) { +func (c *MonitorController) createOrUpdateMonitor(monitorService MonitorServiceProxy, monitorName string, monitorURL string, annotations map[string]string) { m, _ := monitorService.GetByName(monitorName) if m != nil { // Monitor Already Exists @@ -217,11 +217,16 @@ func (c *MonitorController) createOrUpdateMonitor(monitorService MonitorServiceP if m.url != monitorURL { // Monitor does not have the same url // update the monitor with the new url m.url = monitorURL + m.annotations = annotations monitorService.Update(*m) } } else { // Create a new monitor for this ingress - m := Monitor{name: monitorName, url: monitorURL} + m := Monitor{ + name: monitorName, + url: monitorURL, + annotations: annotations, + } monitorService.Add(m) } } diff --git a/src/controller_test.go b/src/controller_test.go index 1e0a46d5..90dd1986 100644 --- a/src/controller_test.go +++ b/src/controller_test.go @@ -11,6 +11,8 @@ import ( "k8s.io/api/extensions/v1beta1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) func TestAddIngressWithNoAnnotationShouldNotCreateMonitor(t *testing.T) { @@ -28,7 +30,7 @@ func TestAddIngressWithNoAnnotationShouldNotCreateMonitor(t *testing.T) { ingress := createIngressObject(ingressName, namespace, url) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -42,7 +44,7 @@ func TestAddIngressWithNoAnnotationShouldNotCreateMonitor(t *testing.T) { // Should not exist checkMonitorWithName(t, monitorName, false) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) } func TestAddIngressWithCorrectMonitorTemplate(t *testing.T) { @@ -63,7 +65,7 @@ func TestAddIngressWithCorrectMonitorTemplate(t *testing.T) { ingress := createIngressObject(ingressName, namespace, url) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -78,7 +80,7 @@ func TestAddIngressWithCorrectMonitorTemplate(t *testing.T) { // Should not exist checkMonitorWithName(t, monitorName, false) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) } func TestInvalidMonitorTemplateShouldPanic(t *testing.T) { @@ -106,7 +108,7 @@ func TestAddIngressWithAnnotationEnabledShouldCreateMonitorAndDelete(t *testing. ingress = addMonitorAnnotationToIngress(ingress, true) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -120,7 +122,7 @@ func TestAddIngressWithAnnotationEnabledShouldCreateMonitorAndDelete(t *testing. // Should exist checkMonitorWithName(t, monitorName, true) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) time.Sleep(5 * time.Second) @@ -143,7 +145,7 @@ func TestAddIngressWithAnnotationDisabledShouldNotCreateMonitor(t *testing.T) { ingress = addMonitorAnnotationToIngress(ingress, false) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -157,7 +159,7 @@ func TestAddIngressWithAnnotationDisabledShouldNotCreateMonitor(t *testing.T) { // Should not exist checkMonitorWithName(t, monitorName, false) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) } func TestUpdateIngressWithAnnotationDisabledShouldNotCreateMonitor(t *testing.T) { @@ -169,7 +171,7 @@ func TestUpdateIngressWithAnnotationDisabledShouldNotCreateMonitor(t *testing.T) ingress := createIngressObject(ingressName, namespace, url) - ingress, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + ingress, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -184,7 +186,7 @@ func TestUpdateIngressWithAnnotationDisabledShouldNotCreateMonitor(t *testing.T) ingress = addMonitorAnnotationToIngress(ingress, false) - ingress, err = controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) + ingress, err = controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) if err != nil { panic(err) } @@ -195,7 +197,7 @@ func TestUpdateIngressWithAnnotationDisabledShouldNotCreateMonitor(t *testing.T) // Should not exist checkMonitorWithName(t, monitorName, false) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) } func TestUpdateIngressWithAnnotationEnabledShouldCreateMonitorAndDelete(t *testing.T) { @@ -207,7 +209,7 @@ func TestUpdateIngressWithAnnotationEnabledShouldCreateMonitorAndDelete(t *testi ingress := createIngressObject(ingressName, namespace, url) - ingress, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + ingress, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -222,7 +224,7 @@ func TestUpdateIngressWithAnnotationEnabledShouldCreateMonitorAndDelete(t *testi ingress = addMonitorAnnotationToIngress(ingress, true) - ingress, err = controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) + ingress, err = controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) if err != nil { panic(err) } @@ -235,7 +237,7 @@ func TestUpdateIngressWithAnnotationEnabledShouldCreateMonitorAndDelete(t *testi // Should exist checkMonitorWithName(t, monitorName, true) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) time.Sleep(3 * time.Second) @@ -254,7 +256,7 @@ func TestUpdateIngressWithAnnotationFromEnabledToDisabledShouldDeleteMonitor(t * ingress = addMonitorAnnotationToIngress(ingress, true) - ingress, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + ingress, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -276,7 +278,7 @@ func TestUpdateIngressWithAnnotationFromEnabledToDisabledShouldDeleteMonitor(t * ingress = updateMonitorAnnotationInIngress(ingress, false) - ingress, err = controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) + ingress, err = controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) if err != nil { panic(err) } @@ -287,7 +289,7 @@ func TestUpdateIngressWithAnnotationFromEnabledToDisabledShouldDeleteMonitor(t * // Should not exist checkMonitorWithName(t, monitorName, false) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) } func TestUpdateIngressWithNewURLShouldUpdateMonitor(t *testing.T) { @@ -302,7 +304,7 @@ func TestUpdateIngressWithNewURLShouldUpdateMonitor(t *testing.T) { ingress = addMonitorAnnotationToIngress(ingress, true) - ingress, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + ingress, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -325,7 +327,7 @@ func TestUpdateIngressWithNewURLShouldUpdateMonitor(t *testing.T) { // Update url ingress.Spec.Rules[0].Host = newUrl - ingress, err = controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) + ingress, err = controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) if err != nil { panic(err) } @@ -351,7 +353,7 @@ func TestUpdateIngressWithNewURLShouldUpdateMonitor(t *testing.T) { t.Error("Monitor did not update") } - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) time.Sleep(3 * time.Second) @@ -370,7 +372,7 @@ func TestUpdateIngressWithEnabledAnnotationShouldCreateMonitorAndDelete(t *testi ingress = addMonitorAnnotationToIngress(ingress, false) - ingress, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + ingress, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -385,7 +387,7 @@ func TestUpdateIngressWithEnabledAnnotationShouldCreateMonitorAndDelete(t *testi ingress = updateMonitorAnnotationInIngress(ingress, true) - ingress, err = controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) + ingress, err = controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Update(ingress) if err != nil { panic(err) } @@ -398,7 +400,7 @@ func TestUpdateIngressWithEnabledAnnotationShouldCreateMonitorAndDelete(t *testi // Should exist checkMonitorWithName(t, monitorName, true) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) time.Sleep(3 * time.Second) @@ -421,7 +423,7 @@ func TestAddIngressWithAnnotationEnabledButDisableDeletionShouldCreateMonitorAnd ingress = addMonitorAnnotationToIngress(ingress, true) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -435,7 +437,7 @@ func TestAddIngressWithAnnotationEnabledButDisableDeletionShouldCreateMonitorAnd // Should exist checkMonitorWithName(t, monitorName, true) - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) time.Sleep(5 * time.Second) @@ -465,11 +467,11 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasPodShouldCreateMonit service := createServiceObject(serviceName, podName, namespace) - if _, err := controller.clientset.Pods(namespace).Create(pod); err != nil { + if _, err := controller.kubeClient.Core().Pods(namespace).Create(pod); err != nil { panic(err) } - if _, err := controller.clientset.Services(namespace).Create(service); err != nil { + if _, err := controller.kubeClient.Core().Services(namespace).Create(service); err != nil { panic(err) } @@ -479,7 +481,7 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasPodShouldCreateMonit ingress = addServiceToIngress(ingress, serviceName, 80) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -501,11 +503,11 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasPodShouldCreateMonit t.Error("An error occured while getting monitor") } - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) - controller.clientset.Pods(namespace).Delete(podName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Pods(namespace).Delete(podName, &meta_v1.DeleteOptions{}) - controller.clientset.Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) time.Sleep(15 * time.Second) @@ -537,11 +539,11 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasPodButNoProbesShould service := createServiceObject(serviceName, podName, namespace) - if _, err := controller.clientset.Pods(namespace).Create(pod); err != nil { + if _, err := controller.kubeClient.Core().Pods(namespace).Create(pod); err != nil { panic(err) } - if _, err := controller.clientset.Services(namespace).Create(service); err != nil { + if _, err := controller.kubeClient.Core().Services(namespace).Create(service); err != nil { panic(err) } @@ -551,7 +553,7 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasPodButNoProbesShould ingress = addServiceToIngress(ingress, serviceName, 80) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -573,11 +575,11 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasPodButNoProbesShould t.Error("An error occured while getting monitor") } - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) - controller.clientset.Pods(namespace).Delete(podName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Pods(namespace).Delete(podName, &meta_v1.DeleteOptions{}) - controller.clientset.Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) time.Sleep(15 * time.Second) @@ -609,11 +611,11 @@ func TestAddIngressWithHealthAnnotationAssociatedWithServiceAndHasPodShouldCreat service := createServiceObject(serviceName, podName, namespace) - if _, err := controller.clientset.Pods(namespace).Create(pod); err != nil { + if _, err := controller.kubeClient.Core().Pods(namespace).Create(pod); err != nil { panic(err) } - if _, err := controller.clientset.Services(namespace).Create(service); err != nil { + if _, err := controller.kubeClient.Core().Services(namespace).Create(service); err != nil { panic(err) } @@ -625,7 +627,7 @@ func TestAddIngressWithHealthAnnotationAssociatedWithServiceAndHasPodShouldCreat ingress = addServiceToIngress(ingress, serviceName, 80) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -647,11 +649,11 @@ func TestAddIngressWithHealthAnnotationAssociatedWithServiceAndHasPodShouldCreat t.Error("An error occured while getting monitor") } - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) - controller.clientset.Pods(namespace).Delete(podName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Pods(namespace).Delete(podName, &meta_v1.DeleteOptions{}) - controller.clientset.Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) time.Sleep(15 * time.Second) @@ -681,7 +683,7 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasNoPodShouldCreateMon service := createServiceObject(serviceName, podName, namespace) - if _, err := controller.clientset.Services(namespace).Create(service); err != nil { + if _, err := controller.kubeClient.Core().Services(namespace).Create(service); err != nil { panic(err) } @@ -691,7 +693,7 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasNoPodShouldCreateMon ingress = addServiceToIngress(ingress, serviceName, 80) - result, err := controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) + result, err := controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Create(ingress) if err != nil { panic(err) @@ -713,9 +715,9 @@ func TestAddIngressWithAnnotationAssociatedWithServiceAndHasNoPodShouldCreateMon t.Error("An error occured while getting monitor") } - controller.clientset.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) + controller.kubeClient.ExtensionsV1beta1().Ingresses(namespace).Delete(ingressName, &meta_v1.DeleteOptions{}) - controller.clientset.Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) + controller.kubeClient.Core().Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) time.Sleep(15 * time.Second) @@ -798,10 +800,7 @@ func getMonitorService() *UpTimeMonitorService { config := getControllerConfig() service := UpTimeMonitorService{} - apiKey := config.Providers[0].ApiKey - alertContacts := config.Providers[0].AlertContacts - url := config.Providers[0].ApiURL - service.Setup(apiKey, url, alertContacts) + service.Setup(config.Providers[0]) return &service } @@ -913,11 +912,13 @@ func removeMonitorAnnotationFromIngress(ingress *v1beta1.Ingress) *v1beta1.Ingre } func getControllerWithNamespace(namespace string, enableDeletion bool) *MonitorController { - // create the in-cluster config - clusterConfig := createInClusterConfig() - - // create the clientset - clientset := createKubernetesClient(clusterConfig) + var kubeClient kubernetes.Interface + _, err := rest.InClusterConfig() + if err != nil { + kubeClient = GetClientOutOfCluster() + } else { + kubeClient = GetClient() + } // fetche and create controller config from file config := getControllerConfig() @@ -925,7 +926,7 @@ func getControllerWithNamespace(namespace string, enableDeletion bool) *MonitorC config.EnableMonitorDeletion = enableDeletion // create the monitoring controller - controller := NewMonitorController(namespace, clientset, config) + controller := NewMonitorController(namespace, kubeClient, config) return controller } diff --git a/src/ingress-wrapper.go b/src/ingress-wrapper.go index 8843cb4d..c61c96ad 100644 --- a/src/ingress-wrapper.go +++ b/src/ingress-wrapper.go @@ -10,9 +10,9 @@ import ( ) type IngressWrapper struct { - ingress *v1beta1.Ingress - namespace string - clientset *kubernetes.Clientset + ingress *v1beta1.Ingress + namespace string + kubeClient kubernetes.Interface } func (iw *IngressWrapper) supportsTLS() bool { @@ -122,7 +122,7 @@ func (iw *IngressWrapper) tryGetHealthEndpointFromIngress() (string, bool) { return "", false } - service, err := iw.clientset.Core().Services(iw.namespace).Get(serviceName, meta_v1.GetOptions{}) + service, err := iw.kubeClient.Core().Services(iw.namespace).Get(serviceName, meta_v1.GetOptions{}) if err != nil { log.Printf("Get service from kubernetes cluster error:%v", err) return "", false @@ -130,7 +130,7 @@ func (iw *IngressWrapper) tryGetHealthEndpointFromIngress() (string, bool) { set := labels.Set(service.Spec.Selector) - if pods, err := iw.clientset.Core().Pods(iw.namespace).List(meta_v1.ListOptions{LabelSelector: set.AsSelector().String()}); err != nil { + if pods, err := iw.kubeClient.Core().Pods(iw.namespace).List(meta_v1.ListOptions{LabelSelector: set.AsSelector().String()}); err != nil { log.Printf("List Pods of service[%s] error:%v", service.GetName(), err) } else if len(pods.Items) > 0 { pod := pods.Items[0] diff --git a/src/kube.go b/src/kube.go new file mode 100644 index 00000000..cac09fb9 --- /dev/null +++ b/src/kube.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + + "github.com/sirupsen/logrus" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// GetClient returns a k8s clientset to the request from inside of cluster +func GetClient() kubernetes.Interface { + config, err := rest.InClusterConfig() + if err != nil { + logrus.Fatalf("Can not get kubernetes config: %v", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + logrus.Fatalf("Can not create kubernetes client: %v", err) + } + + return clientset +} + +func buildOutOfClusterConfig() (*rest.Config, error) { + kubeconfigPath := os.Getenv("KUBECONFIG") + if kubeconfigPath == "" { + kubeconfigPath = os.Getenv("HOME") + "/.kube/config" + } + return clientcmd.BuildConfigFromFlags("", kubeconfigPath) +} + +// GetClientOutOfCluster returns a k8s clientset to the request from outside of cluster +func GetClientOutOfCluster() kubernetes.Interface { + config, err := buildOutOfClusterConfig() + if err != nil { + logrus.Fatalf("Can not get kubernetes config: %v", err) + } + + clientset, err := kubernetes.NewForConfig(config) + + return clientset +} diff --git a/src/main.go b/src/main.go index 9fbf8b2f..165425fb 100644 --- a/src/main.go +++ b/src/main.go @@ -14,17 +14,19 @@ func main() { log.Fatal("Could not find the current namespace") } - // create the in-cluster config - clusterConfig := createInClusterConfig() - - // create the clientset - clientset := createKubernetesClient(clusterConfig) + var kubeClient kubernetes.Interface + _, err := rest.InClusterConfig() + if err != nil { + kubeClient = GetClientOutOfCluster() + } else { + kubeClient = GetClient() + } // fetche and create controller config from file config := getControllerConfig() // create the monitoring controller - controller := NewMonitorController(currentNamespace, clientset, config) + controller := NewMonitorController(currentNamespace, kubeClient, config) // Now let's start the controller stop := make(chan struct{}) diff --git a/src/monitor-models.go b/src/monitor-models.go index 21d30908..756f3cbf 100644 --- a/src/monitor-models.go +++ b/src/monitor-models.go @@ -1,7 +1,8 @@ package main type Monitor struct { - url string - name string - id string + url string + name string + id string + annotations map[string]string } diff --git a/src/monitor-proxy.go b/src/monitor-proxy.go index 006fb72f..b3a072fd 100644 --- a/src/monitor-proxy.go +++ b/src/monitor-proxy.go @@ -12,14 +12,16 @@ func (mp *MonitorServiceProxy) OfType(mType string) MonitorServiceProxy { switch mType { case "UptimeRobot": mp.monitor = &UpTimeMonitorService{} + case "Pingdom": + mp.monitor = &PingdomMonitorService{} default: - log.Panic("No such provider found") + log.Panic("No such provider found: ", mType) } return *mp } -func (mp *MonitorServiceProxy) Setup(apiKey string, url string, alertContacts string) { - mp.monitor.Setup(apiKey, url, alertContacts) +func (mp *MonitorServiceProxy) Setup(p Provider) { + mp.monitor.Setup(p) } func (mp *MonitorServiceProxy) GetAll() []Monitor { diff --git a/src/monitor-service.go b/src/monitor-service.go index fd6a84c5..bd95cd74 100644 --- a/src/monitor-service.go +++ b/src/monitor-service.go @@ -6,5 +6,5 @@ type MonitorService interface { Update(m Monitor) GetByName(name string) (*Monitor, error) Remove(m Monitor) - Setup(apiKey string, url string, alertContacts string) + Setup(p Provider) } diff --git a/src/pingdom-monitor.go b/src/pingdom-monitor.go new file mode 100644 index 00000000..b6a704b7 --- /dev/null +++ b/src/pingdom-monitor.go @@ -0,0 +1,182 @@ +package main + +import ( + "fmt" + "log" + "net/url" + "strconv" + "strings" + + "github.com/russellcardullo/go-pingdom/pingdom" +) + +const ( + PingdomResolutionAnnotation = "monitor.stakater.com/pingdom/resolution" + PingdomSendNotificationWhenDownAnnotation = "monitor.stakater.com/pingdom/send-notification-when-down" + PingdomPausedAnnotation = "monitor.stakater.com/pingdom/paused" + PingdomNotifyWhenBackUpAnnotation = "monitor.stakater.com/pingdom/notify-when-back-up" +) + +// PingdomMonitorService interfaces with MonitorService +type PingdomMonitorService struct { + apiKey string + url string + alertContacts string + username string + password string + client *pingdom.Client +} + +func (service *PingdomMonitorService) Setup(p Provider) { + service.apiKey = p.ApiKey + service.url = p.ApiURL + service.alertContacts = p.AlertContacts + service.username = p.Username + service.password = p.Password + service.client = pingdom.NewClient(service.username, service.password, service.apiKey) +} + +func (service *PingdomMonitorService) GetByName(name string) (*Monitor, error) { + var match *Monitor + + monitors := service.GetAll() + for _, mon := range monitors { + if mon.name == name { + match = &mon + } + } + + if match == nil { + return match, fmt.Errorf("Unable to locate monitor with name %v", name) + } + + return match, nil +} + +func (service *PingdomMonitorService) GetAll() []Monitor { + var monitors []Monitor + + checks, err := service.client.Checks.List() + if err != nil { + log.Println("Error recevied while listing checks: ", err.Error()) + return nil + } + for _, mon := range checks { + newMon := Monitor{ + url: mon.Hostname, + id: fmt.Sprintf("%v", mon.ID), + name: mon.Name, + } + monitors = append(monitors, newMon) + } + + return monitors +} + +func (service *PingdomMonitorService) Add(m Monitor) { + httpCheck := service.createHttpCheck(m) + + _, err := service.client.Checks.Create(&httpCheck) + if err != nil { + log.Println("Error Adding Monitor: ", err.Error()) + } else { + log.Println("Added monitor for: ", m.name) + } +} + +func (service *PingdomMonitorService) Update(m Monitor) { + httpCheck := service.createHttpCheck(m) + monitorID, _ := strconv.Atoi(m.id) + + resp, err := service.client.Checks.Update(monitorID, &httpCheck) + if err != nil { + log.Println("Error updating Monitor: ", err.Error()) + } else { + log.Println("Updated Monitor: ", resp) + } +} + +func (service *PingdomMonitorService) Remove(m Monitor) { + monitorID, _ := strconv.Atoi(m.id) + + resp, err := service.client.Checks.Delete(monitorID) + if err != nil { + log.Println("Error deleting Monitor: ", err.Error()) + } else { + log.Println("Delete Monitor: ", resp) + } +} + +func (service *PingdomMonitorService) createHttpCheck(monitor Monitor) pingdom.HttpCheck { + httpCheck := pingdom.HttpCheck{} + url, err := url.Parse(monitor.url) + if err != nil { + log.Println("Unable to parse the URL: ", service.url) + } + + if url.Scheme == "https" { + httpCheck.Encryption = true + } else { + httpCheck.Encryption = false + } + + httpCheck.Hostname = url.Host + httpCheck.Url = url.Path + httpCheck.Name = monitor.name + + userIdsStringArray := strings.Split(service.alertContacts, "-") + + if userIds, err := sliceAtoi(userIdsStringArray); err != nil { + log.Println(err.Error()) + } else { + httpCheck.UserIds = userIds + } + + service.addAnnotationConfigToHttpCheck(&httpCheck, monitor.annotations) + + return httpCheck +} + +func (service *PingdomMonitorService) addAnnotationConfigToHttpCheck(httpCheck *pingdom.HttpCheck, annotations map[string]string) { + // Read known annotations, try to map them to pingdom configs + // set some default values if we can't find them + + if value, ok := annotations[PingdomNotifyWhenBackUpAnnotation]; ok { + boolValue, err := strconv.ParseBool(value) + if err == nil { + httpCheck.NotifyWhenBackup = boolValue + } + } + + if value, ok := annotations[PingdomPausedAnnotation]; ok { + boolValue, err := strconv.ParseBool(value) + if err == nil { + httpCheck.Paused = boolValue + } + } + + if value, ok := annotations[PingdomResolutionAnnotation]; ok { + intValue, err := strconv.Atoi(value) + if err == nil { + httpCheck.Resolution = intValue + } else { + log.Println("Error decoding input into an integer") + httpCheck.Resolution = 1 + } + } else { + httpCheck.Resolution = 1 + } + + if value, ok := annotations[PingdomSendNotificationWhenDownAnnotation]; ok { + intValue, err := strconv.Atoi(value) + if err == nil { + httpCheck.SendNotificationWhenDown = intValue + } else { + log.Println("Error decoding input into an integer") + httpCheck.SendNotificationWhenDown = 3 + } + } else { + httpCheck.SendNotificationWhenDown = 3 + } + +} diff --git a/src/pingdom-monitor_test.go b/src/pingdom-monitor_test.go new file mode 100644 index 00000000..16dcb722 --- /dev/null +++ b/src/pingdom-monitor_test.go @@ -0,0 +1,78 @@ +package main + +// import "testing" + +// func TestAddPingdomMonitorWithCorrectValues(t *testing.T) { +// config := getControllerConfig() + +// service := PingdomMonitorService{} +// service.Setup(config.Providers[1]) + +// m := Monitor{name: "google-test", url: "https://google.com"} +// service.Add(m) + +// mRes, err := service.GetByName("google-test") + +// if err != nil { +// t.Error("Error: " + err.Error()) +// } +// if mRes.name != m.name || mRes.url != m.url { +// t.Error("URL and name should be the same") +// } +// service.Remove(*mRes) +// } + +// func TestUpdateMonitorWithCorrectValues(t *testing.T) { +// config := getControllerConfig() + +// service := PingdomMonitorService{} +// service.Setup(config.Providers[1]) + +// m := Monitor{name: "google-test", url: "https://google.com"} +// service.Add(m) + +// mRes, err := service.GetByName("google-test") + +// if err != nil { +// t.Error("Error: " + err.Error()) +// } +// if mRes.name != m.name || mRes.url != m.url { +// t.Error("URL and name should be the same") +// } + +// mRes.url = "https://facebook.com" + +// service.Update(*mRes) + +// mRes, err = service.GetByName("google-test") + +// if err != nil { +// t.Error("Error: " + err.Error()) +// } +// if mRes.url != "https://facebook.com" { +// t.Error("URL and name should be the same") +// } + +// service.Remove(*mRes) +// } + +// func TestAddMonitorWithIncorrectValues(t *testing.T) { +// config := getControllerConfig() + +// service := PingdomMonitorService{} +// config.Providers[1].ApiKey = "dummy-api-key" +// service.Setup(config.Providers[0]) + +// m := Monitor{name: "google-test", url: "https://google.com"} +// service.Add(m) + +// mRes, err := service.GetByName("google-test") + +// if err != nil { +// t.Error("Error: " + err.Error()) +// } + +// if mRes != nil { +// t.Error("Monitor should not be added") +// } +// } diff --git a/src/uptime-monitor.go b/src/uptime-monitor.go index 489c436e..7c76dbe9 100644 --- a/src/uptime-monitor.go +++ b/src/uptime-monitor.go @@ -13,10 +13,10 @@ type UpTimeMonitorService struct { alertContacts string } -func (monitor *UpTimeMonitorService) Setup(apiKey string, url string, alertContacts string) { - monitor.apiKey = apiKey - monitor.url = url - monitor.alertContacts = alertContacts +func (monitor *UpTimeMonitorService) Setup(p Provider) { + monitor.apiKey = p.ApiKey + monitor.url = p.ApiURL + monitor.alertContacts = p.AlertContacts } func (monitor *UpTimeMonitorService) GetByName(name string) (*Monitor, error) { diff --git a/src/uptime-monitor_test.go b/src/uptime-monitor_test.go index 26f68bd2..982e1159 100644 --- a/src/uptime-monitor_test.go +++ b/src/uptime-monitor_test.go @@ -8,10 +8,7 @@ func TestAddMonitorWithCorrectValues(t *testing.T) { config := getControllerConfig() service := UpTimeMonitorService{} - apiKey := config.Providers[0].ApiKey - alertContacts := config.Providers[0].AlertContacts - url := config.Providers[0].ApiURL - service.Setup(apiKey, url, alertContacts) + service.Setup(config.Providers[0]) m := Monitor{name: "google-test", url: "https://google.com"} service.Add(m) @@ -31,10 +28,7 @@ func TestUpdateMonitorWithCorrectValues(t *testing.T) { config := getControllerConfig() service := UpTimeMonitorService{} - apiKey := config.Providers[0].ApiKey - alertContacts := config.Providers[0].AlertContacts - url := config.Providers[0].ApiURL - service.Setup(apiKey, url, alertContacts) + service.Setup(config.Providers[0]) m := Monitor{name: "google-test", url: "https://google.com"} service.Add(m) @@ -68,10 +62,8 @@ func TestAddMonitorWithIncorrectValues(t *testing.T) { config := getControllerConfig() service := UpTimeMonitorService{} - apiKey := "dummy-api-key" - alertContacts := config.Providers[0].AlertContacts - url := config.Providers[0].ApiURL - service.Setup(apiKey, url, alertContacts) + config.Providers[0].ApiKey = "dummy-api-key" + service.Setup(config.Providers[0]) m := Monitor{name: "google-test", url: "https://google.com"} service.Add(m) diff --git a/src/util.go b/src/util.go new file mode 100644 index 00000000..e95836d2 --- /dev/null +++ b/src/util.go @@ -0,0 +1,17 @@ +package main + +import "strconv" + +func sliceAtoi(stringSlice []string) ([]int, error) { + var intSlice = []int{} + + for _, stringValue := range stringSlice { + intValue, err := strconv.Atoi(stringValue) + if err != nil { + return intSlice, err + } + intSlice = append(intSlice, intValue) + } + + return intSlice, nil +}