diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2c92871 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +vendor/ +_output/ +examples/ +*.md diff --git a/.gitignore b/.gitignore index 8872d8e..5f6e0ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .idea/ vendor/ -pod-reaper.iml - +_output/ +pod-reaper.iml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index dcb18ed..8dc53cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,9 @@ FROM golang:1.14 AS build WORKDIR /go/src/github.com/target/pod-reaper ENV CGO_ENABLED=0 GOOS=linux COPY ./ ./ -RUN go test ./rules/... && \ - go test ./reaper/... && \ - go build -o pod-reaper -a -installsuffix go ./reaper +RUN go test ./cmd/... && \ + go test ./internal/... && \ + go build -o pod-reaper -a -installsuffix go ./cmd/pod-reaper # Application FROM scratch diff --git a/Dockerfile-minikube b/Dockerfile-minikube index 20a175e..c9e6c76 100644 --- a/Dockerfile-minikube +++ b/Dockerfile-minikube @@ -1,5 +1,5 @@ # including this because default minikube drivers do not yet support multistage docker builds # this will only be used for minikube FROM scratch -COPY pod-reaper / +COPY _output/bin/pod-reaper / CMD ["/pod-reaper"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c660230 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: test + +VERSION?=$(shell git describe --tags) +REPOSITORY=target +IMAGE:=pod-reaper:$(VERSION) + +CLUSTER_NAME=e2e + +all: build + +build: + CGO_ENABLED=0 go build -o _output/bin/pod-reaper github.com/target/pod-reaper/cmd/pod-reaper + +image: + docker build -t $(IMAGE) . + +clean: + rm -rf _output + +test-unit: + ./test/run-unit-tests.sh + +test-e2e: + ./test/create-cluster.sh $(CLUSTER_NAME) + ./test/run-e2e-tests.sh + ./test/delete-cluster.sh $(CLUSTER_NAME) \ No newline at end of file diff --git a/reaper/options.go b/cmd/pod-reaper/app/options.go similarity index 98% rename from reaper/options.go rename to cmd/pod-reaper/app/options.go index a105175..1deb545 100644 --- a/reaper/options.go +++ b/cmd/pod-reaper/app/options.go @@ -1,4 +1,4 @@ -package main +package app import ( "fmt" @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" - "github.com/target/pod-reaper/rules" + "github.com/target/pod-reaper/internal/pkg/rules" ) // environment variable names diff --git a/reaper/options_test.go b/cmd/pod-reaper/app/options_test.go similarity index 99% rename from reaper/options_test.go rename to cmd/pod-reaper/app/options_test.go index 902c220..3a025c1 100644 --- a/reaper/options_test.go +++ b/cmd/pod-reaper/app/options_test.go @@ -1,4 +1,4 @@ -package main +package app import ( "os" diff --git a/reaper/reaper.go b/cmd/pod-reaper/app/reaper.go similarity index 83% rename from reaper/reaper.go rename to cmd/pod-reaper/app/reaper.go index 4afd444..5b414e2 100644 --- a/reaper/reaper.go +++ b/cmd/pod-reaper/app/reaper.go @@ -1,4 +1,4 @@ -package main +package app import ( "time" @@ -9,25 +9,14 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" ) -type reaper struct { - clientSet *kubernetes.Clientset +type Reaper struct { + clientSet kubernetes.Interface options options } -func newReaper() reaper { - config, err := rest.InClusterConfig() - if err != nil { - logrus.WithError(err).Panic("error getting in cluster kubernetes config") - panic(err) - } - clientSet, err := kubernetes.NewForConfig(config) - if err != nil { - logrus.WithError(err).Panic("unable to get client set for in cluster kubernetes config") - panic(err) - } +func NewReaper(clientSet kubernetes.Interface) Reaper { if clientSet == nil { message := "kubernetes client set cannot be nil" logrus.Panic(message) @@ -38,13 +27,14 @@ func newReaper() reaper { logrus.WithError(err).Panic("error loading options") panic(err) } - return reaper{ + + return Reaper{ clientSet: clientSet, options: options, } } -func (reaper reaper) getPods() *v1.PodList { +func (reaper Reaper) getPods() *v1.PodList { coreClient := reaper.clientSet.CoreV1() pods := coreClient.Pods(reaper.options.namespace) listOptions := metav1.ListOptions{} @@ -78,7 +68,7 @@ func (reaper reaper) getPods() *v1.PodList { return podList } -func (reaper reaper) reapPod(pod v1.Pod, reasons []string) { +func (reaper Reaper) reapPod(pod v1.Pod, reasons []string) { deleteOptions := &metav1.DeleteOptions{ GracePeriodSeconds: reaper.options.gracePeriod, } @@ -102,7 +92,7 @@ func (reaper reaper) reapPod(pod v1.Pod, reasons []string) { } } -func (reaper reaper) scytheCycle() { +func (reaper Reaper) scytheCycle() { logrus.Debug("starting reap cycle") pods := reaper.getPods() for _, pod := range pods.Items { @@ -121,7 +111,7 @@ func cronWithOptionalSeconds() *cron.Cron { cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor))) } -func (reaper reaper) harvest() { +func (reaper Reaper) Harvest() { runForever := reaper.options.runDuration == 0 schedule := cronWithOptionalSeconds() _, err := schedule.AddFunc(reaper.options.schedule, func() { diff --git a/reaper/main.go b/cmd/pod-reaper/main.go similarity index 79% rename from reaper/main.go rename to cmd/pod-reaper/main.go index 3838af7..335d790 100644 --- a/reaper/main.go +++ b/cmd/pod-reaper/main.go @@ -5,6 +5,8 @@ import ( joonix "github.com/joonix/log" "github.com/sirupsen/logrus" + "github.com/target/pod-reaper/cmd/pod-reaper/app" + "github.com/target/pod-reaper/internal/pkg/client" ) const envLogLevel = "LOG_LEVEL" @@ -14,13 +16,18 @@ const logrusFormat = "Logrus" const defaultLogLevel = logrus.InfoLevel func main() { - logLevel := getLogLevel() - logrus.SetLevel(logLevel) logFormat := getLogFormat() logrus.SetFormatter(logFormat) + logLevel := getLogLevel() + logrus.SetLevel(logLevel) - reaper := newReaper() - reaper.harvest() + clientset, err := client.CreateClient("") + if err != nil { + logrus.WithError(err).Panic("cannot create client") + panic(err) + } + reaper := app.NewReaper(clientset) + reaper.Harvest() logrus.Info("pod reaper is exiting") } diff --git a/reaper/main_test.go b/cmd/pod-reaper/main_test.go similarity index 100% rename from reaper/main_test.go rename to cmd/pod-reaper/main_test.go diff --git a/go.mod b/go.mod index 62eedf8..6e6537a 100644 --- a/go.mod +++ b/go.mod @@ -11,4 +11,5 @@ require ( k8s.io/api v0.17.0 k8s.io/apimachinery v0.17.3-beta.0 k8s.io/client-go v0.17.0 + sigs.k8s.io/yaml v1.1.0 ) diff --git a/go.sum b/go.sum index 02e0239..b18524d 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,18 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -17,6 +23,7 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= @@ -53,11 +60,13 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/joonix/log v0.0.0-20190130132305-f5f056244ba3 h1:Y4p9xCEVT0gnYoHQwaSb9RYxRRN4WxxOaT5h7xGZBAo= github.com/joonix/log v0.0.0-20190130132305-f5f056244ba3/go.mod h1:9alna084PKap49x3Dl7QTGUXiS37acLi8ryAexT1SJc= diff --git a/internal/pkg/client/client.go b/internal/pkg/client/client.go new file mode 100644 index 0000000..ec79c73 --- /dev/null +++ b/internal/pkg/client/client.go @@ -0,0 +1,66 @@ +/* +Copyright 2017 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "fmt" + + clientset "k8s.io/client-go/kubernetes" + // Ensure to load all auth plugins. + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// CreateClient creates a new Kubernets clientset with the given config or in-cluster config +func CreateClient(kubeconfig string) (clientset.Interface, error) { + var cfg *rest.Config + if len(kubeconfig) != 0 { + master, err := getMasterFromKubeconfig(kubeconfig) + if err != nil { + return nil, fmt.Errorf("Failed to parse kubeconfig file: %v ", err) + } + + cfg, err = clientcmd.BuildConfigFromFlags(master, kubeconfig) + if err != nil { + return nil, fmt.Errorf("Unable to build config: %v", err) + } + + } else { + var err error + cfg, err = rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("Unable to build in cluster config: %v", err) + } + } + + return clientset.NewForConfig(cfg) +} + +func getMasterFromKubeconfig(filename string) (string, error) { + config, err := clientcmd.LoadFromFile(filename) + if err != nil { + return "", err + } + + context, ok := config.Contexts[config.CurrentContext] + if !ok { + return "", fmt.Errorf("Failed to get master address from kubeconfig") + } + + if val, ok := config.Clusters[context.Cluster]; ok { + return val.Server, nil + } + return "", fmt.Errorf("Failed to get master address from kubeconfig") +} diff --git a/internal/pkg/client/kubeutil.go b/internal/pkg/client/kubeutil.go new file mode 100644 index 0000000..b4b2233 --- /dev/null +++ b/internal/pkg/client/kubeutil.go @@ -0,0 +1,93 @@ +package client + +import ( + "fmt" + "io/ioutil" + "time" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + clientset "k8s.io/client-go/kubernetes" + "sigs.k8s.io/yaml" +) + +// KubeUtil contains utility functions to interact with a Kubernetes cluster +type KubeUtil struct { + client clientset.Interface + timeout time.Duration +} + +const retryInterval = 500 * time.Millisecond +const defaultTimeout = 30 * time.Second + +// NewKubeUtil returns a new KubeUtil object that interacts with the given Kubernetes cluster +func NewKubeUtil(client clientset.Interface) KubeUtil { + return KubeUtil{ + client: client, + timeout: defaultTimeout, + } +} + +// ApplyPodManifest applies manifest file to specified namespace +func (k *KubeUtil) ApplyPodManifest(namespace string, path string) (*v1.Pod, error) { + manifest, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var pod *v1.Pod + err = yaml.Unmarshal(manifest, &pod) + if err != nil { + return nil, err + } + + serviceAccountName := pod.Spec.ServiceAccountName + if serviceAccountName == "" { + serviceAccountName = "default" + } + err = k.WaitForServiceAccountExists(namespace, serviceAccountName) + if err != nil { + return nil, err + } + + return k.client.CoreV1().Pods(namespace).Create(pod) +} + +// WaitForPodPhase waits until a pod enters the specified phase or timeout is reached +func (k *KubeUtil) WaitForPodPhase(namespace string, name string, status v1.PodPhase) error { + return wait.PollImmediate(retryInterval, k.timeout, func() (bool, error) { + getOpts := metav1.GetOptions{} + pod, err := k.client.CoreV1().Pods(namespace).Get(name, getOpts) + if err != nil { + return false, fmt.Errorf("error getting pod name %s: %v", name, err) + } + return pod.Status.Phase == status, nil + }) +} + +// WaitForPodToDie waits until pod no longer exists or timeout is reached +func (k *KubeUtil) WaitForPodToDie(namespace string, name string) error { + return wait.PollImmediate(retryInterval, k.timeout, func() (bool, error) { + _, err := k.client.CoreV1().Pods(namespace).Get(name, metav1.GetOptions{}) + if err == nil { + return false, nil + } + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + }) +} + +// WaitForServiceAccountExists waits until the ServiceAccount exists or timeout is reached +func (k *KubeUtil) WaitForServiceAccountExists(namespace string, name string) error { + return wait.PollImmediate(retryInterval, k.timeout, func() (bool, error) { + serviceAccounts := k.client.CoreV1().ServiceAccounts(namespace) + if _, err := serviceAccounts.Get(name, metav1.GetOptions{}); err == nil { + return true, nil + } + return false, nil + }) +} diff --git a/rules/chaos.go b/internal/pkg/rules/chaos.go similarity index 100% rename from rules/chaos.go rename to internal/pkg/rules/chaos.go diff --git a/rules/chaos_test.go b/internal/pkg/rules/chaos_test.go similarity index 98% rename from rules/chaos_test.go rename to internal/pkg/rules/chaos_test.go index 65a397d..0330416 100644 --- a/rules/chaos_test.go +++ b/internal/pkg/rules/chaos_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" ) func TestChaosLoad(t *testing.T) { diff --git a/rules/container_status.go b/internal/pkg/rules/container_status.go similarity index 100% rename from rules/container_status.go rename to internal/pkg/rules/container_status.go diff --git a/rules/container_status_test.go b/internal/pkg/rules/container_status_test.go similarity index 100% rename from rules/container_status_test.go rename to internal/pkg/rules/container_status_test.go diff --git a/rules/duration.go b/internal/pkg/rules/duration.go similarity index 100% rename from rules/duration.go rename to internal/pkg/rules/duration.go diff --git a/rules/duration_test.go b/internal/pkg/rules/duration_test.go similarity index 100% rename from rules/duration_test.go rename to internal/pkg/rules/duration_test.go diff --git a/rules/pod_status.go b/internal/pkg/rules/pod_status.go similarity index 100% rename from rules/pod_status.go rename to internal/pkg/rules/pod_status.go diff --git a/rules/pod_status_test.go b/internal/pkg/rules/pod_status_test.go similarity index 100% rename from rules/pod_status_test.go rename to internal/pkg/rules/pod_status_test.go diff --git a/rules/rules.go b/internal/pkg/rules/rules.go similarity index 100% rename from rules/rules.go rename to internal/pkg/rules/rules.go diff --git a/rules/rules_test.go b/internal/pkg/rules/rules_test.go similarity index 99% rename from rules/rules_test.go rename to internal/pkg/rules/rules_test.go index 71dfc19..11a20bc 100644 --- a/rules/rules_test.go +++ b/internal/pkg/rules/rules_test.go @@ -5,9 +5,10 @@ import ( "testing" "time" + "io/ioutil" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" - "io/ioutil" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/rules/unready.go b/internal/pkg/rules/unready.go similarity index 100% rename from rules/unready.go rename to internal/pkg/rules/unready.go diff --git a/rules/unready_test.go b/internal/pkg/rules/unready_test.go similarity index 100% rename from rules/unready_test.go rename to internal/pkg/rules/unready_test.go diff --git a/test/create-cluster.sh b/test/create-cluster.sh new file mode 100755 index 0000000..ab3edef --- /dev/null +++ b/test/create-cluster.sh @@ -0,0 +1,20 @@ +#!/bin/bash +NAME=${1:-kind} +KUBECONFIG=/tmp/admin.conf + +load_image() { + docker pull $1 + kind load docker-image --name "${NAME}" $1 +} + +kind get clusters | grep "${NAME}" > /dev/null +if [ $? -eq 1 ]; then + kind create cluster --name "${NAME}" +else + echo "Cluster \"${NAME}\" already exists." +fi + +load_image kubernetes/pause +kind get kubeconfig --name "${NAME}" > ${KUBECONFIG} + +echo "Output kubeconfig to ${KUBECONFIG}" \ No newline at end of file diff --git a/test/delete-cluster.sh b/test/delete-cluster.sh new file mode 100755 index 0000000..3591c76 --- /dev/null +++ b/test/delete-cluster.sh @@ -0,0 +1,9 @@ +#!/bin/bash +NAME=${1:-kind} + +kind get clusters | grep "${NAME}" > /dev/null +if [ $? -eq 0 ]; then + kind delete cluster --name "${NAME}" +else + echo "Cluster \"${NAME}\" does not exist." +fi \ No newline at end of file diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 0000000..a7e0f0c --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,90 @@ +package e2e + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/target/pod-reaper/cmd/pod-reaper/app" + "github.com/target/pod-reaper/internal/pkg/client" + v1 "k8s.io/api/core/v1" +) + +const namespace string = "default" +const kubeConfig string = "/tmp/admin.conf" + +func TestChaosRule(t *testing.T) { + clientset, err := client.CreateClient(kubeConfig) + assert.NoError(t, err, "error creating kubernetes client") + kubeUtil := client.NewKubeUtil(clientset) + + // Create pod and wait for it to be running + pod, err := kubeUtil.ApplyPodManifest(namespace, "./pause-pod.yml") + assert.NoError(t, err, "error applying pod manifest") + kubeUtil.WaitForPodPhase(namespace, pod.Name, v1.PodRunning) + + // Run reaper + os.Clearenv() + os.Setenv("NAMESPACE", namespace) + os.Setenv("RUN_DURATION", "1s") + os.Setenv("SCHEDULE", "@every 1s") + os.Setenv("CHAOS_CHANCE", "1") + reaper := app.NewReaper(clientset) + reaper.Harvest() + + // Wait for pod to die so other tests aren't affected + err = kubeUtil.WaitForPodToDie(namespace, pod.Name) + assert.NoError(t, err, "timed out waiting for pod to die") +} + +func TestDurationRule(t *testing.T) { + clientset, err := client.CreateClient(kubeConfig) + assert.NoError(t, err, "error creating kubernetes client") + kubeUtil := client.NewKubeUtil(clientset) + + // Create pod and wait for it to be running + pod, err := kubeUtil.ApplyPodManifest(namespace, "./pause-pod.yml") + assert.NoError(t, err, "error applying pod manifest") + kubeUtil.WaitForPodPhase(namespace, pod.Name, v1.PodRunning) + + // Make sure pod has been running at least 1s + time.Sleep(1 * time.Second) + + // Run reaper + os.Clearenv() + os.Setenv("NAMESPACE", namespace) + os.Setenv("RUN_DURATION", "1s") + os.Setenv("SCHEDULE", "@every 1s") + os.Setenv("MAX_DURATION", "1s") + reaper := app.NewReaper(clientset) + reaper.Harvest() + + // Wait for pod to die so other tests aren't affected + err = kubeUtil.WaitForPodToDie(namespace, pod.Name) + assert.NoError(t, err, "timed out waiting for pod to die") +} + +func TestUnreadyRule(t *testing.T) { + clientset, err := client.CreateClient(kubeConfig) + assert.NoError(t, err, "error creating kubernetes client") + kubeUtil := client.NewKubeUtil(clientset) + + // Create pod and wait for it to be running + pod, err := kubeUtil.ApplyPodManifest(namespace, "./unready-pod.yml") + assert.NoError(t, err, "error applying pod manifest") + kubeUtil.WaitForPodPhase(namespace, pod.Name, v1.PodRunning) + + // Run reaper + os.Clearenv() + os.Setenv("NAMESPACE", namespace) + os.Setenv("RUN_DURATION", "1s") + os.Setenv("SCHEDULE", "@every 1s") + os.Setenv("MAX_UNREADY", "1s") + reaper := app.NewReaper(clientset) + reaper.Harvest() + + // Wait for pod to die so other tests aren't affected + err = kubeUtil.WaitForPodToDie(namespace, pod.Name) + assert.NoError(t, err, "timed out waiting for pod to die") +} diff --git a/test/e2e/pause-pod.yml b/test/e2e/pause-pod.yml new file mode 100644 index 0000000..782c2f5 --- /dev/null +++ b/test/e2e/pause-pod.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pause +spec: + containers: + - name: pause + image: kubernetes/pause + imagePullPolicy: Never + restartPolicy: Never diff --git a/test/e2e/unready-pod.yml b/test/e2e/unready-pod.yml new file mode 100644 index 0000000..bf49ed8 --- /dev/null +++ b/test/e2e/unready-pod.yml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: unready +spec: + containers: + - name: unready + image: kubernetes/pause + imagePullPolicy: Never + readinessProbe: + httpGet: + path: /ready + port: 80 + initialDelaySeconds: 0 + periodSeconds: 1 + failureThreshold: 1 + periodSeconds: 1 + restartPolicy: Never diff --git a/test/run-e2e-tests.sh b/test/run-e2e-tests.sh new file mode 100755 index 0000000..3718492 --- /dev/null +++ b/test/run-e2e-tests.sh @@ -0,0 +1,21 @@ +# Copyright 2017 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/bin/bash + +# This just run e2e tests. +PRJ_PREFIX="github.com/target/pod-reaper" + +go clean -testcache +go test ${PRJ_PREFIX}/test/e2e/ -v \ No newline at end of file diff --git a/test/run-unit-tests.sh b/test/run-unit-tests.sh new file mode 100755 index 0000000..a6029e9 --- /dev/null +++ b/test/run-unit-tests.sh @@ -0,0 +1,19 @@ +# Copyright 2017 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/bin/bash + +# This just run unit-tests. Ignoring the current directory so as to avoid running e2e tests. +PRJ_PREFIX="github.com/target/pod-reaper" +go test $(go list ${PRJ_PREFIX}/... | grep -v ${PRJ_PREFIX}/vendor/| grep -v ${PRJ_PREFIX}/test/) \ No newline at end of file