466 lines
12 KiB
Go
466 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sanity-io/litter"
|
|
cronjobv1 "k8s.io/api/batch/v1"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
applyv1 "k8s.io/client-go/applyconfigurations/batch/v1"
|
|
"k8s.io/client-go/discovery"
|
|
"k8s.io/client-go/kubernetes"
|
|
batchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
|
|
"k8s.io/client-go/rest"
|
|
)
|
|
|
|
func Test_Main(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
connectFunc func(provider ConfigProvider) (Client, error)
|
|
exitFunc func(code int)
|
|
}{
|
|
{
|
|
name: "error connecting to K8S",
|
|
connectFunc: func(ConfigProvider) (Client, error) {
|
|
return nil, errors.New("error")
|
|
},
|
|
exitFunc: func(code int) {
|
|
if code != 1 {
|
|
t.Errorf("main() got %d, want 1", code)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
os.Args = []string{"dummy", "--slack-url", "https://dummy.example.org"}
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
exitFunc = tt.exitFunc
|
|
main()
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_doMain(t *testing.T) {
|
|
type args struct {
|
|
slackUrl string
|
|
provider ClientProvider
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
checkFunc func(client Client, slackUrl string, ic chan os.Signal, sleepTime time.Duration, out io.Writer) error
|
|
want int
|
|
}{
|
|
{
|
|
name: "error checking",
|
|
args: args{
|
|
provider: &brokenClientProvider{},
|
|
},
|
|
checkFunc: func(client Client, slackUrl string, ic chan os.Signal, sleepTime time.Duration, out io.Writer) error {
|
|
return errors.New("error")
|
|
},
|
|
want: 1,
|
|
},
|
|
{
|
|
name: "success",
|
|
args: args{
|
|
provider: &brokenClientProvider{},
|
|
},
|
|
checkFunc: func(client Client, slackUrl string, ic chan os.Signal, sleepTime time.Duration, out io.Writer) error {
|
|
return nil
|
|
},
|
|
want: 0,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
checkFunc = tt.checkFunc
|
|
if got := doMain(tt.args.slackUrl, tt.args.provider); got != tt.want {
|
|
t.Errorf("doMain() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_doCheck(t *testing.T) {
|
|
type args struct {
|
|
client Client
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
timeout time.Duration
|
|
slackResponse string
|
|
wantErr bool
|
|
wantOut []string
|
|
}{
|
|
{
|
|
name: "error getting cronjobs",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return nil, errors.New("error")
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "no cronjobs",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: time.Second,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "suspended cronjobs are ignored",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{
|
|
Items: []cronjobv1.CronJob{
|
|
{
|
|
Spec: cronjobv1.CronJobSpec{Suspend: boolP(true)},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: time.Second,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid cron schedule",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{
|
|
Items: []cronjobv1.CronJob{
|
|
{
|
|
Spec: cronjobv1.CronJobSpec{Schedule: "abc"},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "only correctly running cronjobs",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{
|
|
Items: []cronjobv1.CronJob{
|
|
{
|
|
ObjectMeta: v1.ObjectMeta{CreationTimestamp: v1.Time{Time: time.Now()}},
|
|
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *", Suspend: boolP(false)},
|
|
},
|
|
{
|
|
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
|
|
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now()}},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: time.Second,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "error in Slack call",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{
|
|
Items: []cronjobv1.CronJob{
|
|
{
|
|
ObjectMeta: v1.ObjectMeta{Name: "some-name", Namespace: "some-ns"},
|
|
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
|
|
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now().Add(-3 * time.Minute)}},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: time.Second,
|
|
slackResponse: "dummy",
|
|
wantErr: false,
|
|
wantOut: []string{"Checking some-ns/some-name since", "some-ns/some-name was not scheduled. Sending Slack notification.", "Unable to send Slack notification: slack: request failed statuscode: 200, message: invalid character 'd' looking for beginning of value"},
|
|
},
|
|
{
|
|
name: "Slack response not ok",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{
|
|
Items: []cronjobv1.CronJob{
|
|
{
|
|
ObjectMeta: v1.ObjectMeta{Name: "some-name", Namespace: "some-ns"},
|
|
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
|
|
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now().Add(-3 * time.Minute)}},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: time.Second,
|
|
slackResponse: `{"ok": false, "error": "Something went wrong"}`,
|
|
wantErr: false,
|
|
wantOut: []string{"Checking some-ns/some-name since", "some-ns/some-name was not scheduled. Sending Slack notification.", "Unable to send Slack notification: slack: request failed statuscode: 200, message: Something went wrong"},
|
|
},
|
|
{
|
|
name: "Slack response ok",
|
|
args: args{
|
|
client: &brokenClient{
|
|
batchApi: &batchApi{
|
|
cronApi: &cronApi{
|
|
listFn: func(_ context.Context, _ v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return &cronjobv1.CronJobList{
|
|
Items: []cronjobv1.CronJob{
|
|
{
|
|
ObjectMeta: v1.ObjectMeta{Name: "some-name", Namespace: "some-ns"},
|
|
Spec: cronjobv1.CronJobSpec{Schedule: "* * * * *"},
|
|
Status: cronjobv1.CronJobStatus{LastScheduleTime: &v1.Time{Time: time.Now().Add(-3 * time.Minute)}},
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
timeout: time.Second,
|
|
slackResponse: `{"ok": true}`,
|
|
wantErr: false,
|
|
wantOut: []string{"Checking some-ns/some-name since", "some-ns/some-name was not scheduled. Sending Slack notification."},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ic := make(chan os.Signal, 1)
|
|
if tt.timeout > 0 {
|
|
timeout := tt.timeout
|
|
fmt.Printf("Waiting %s before terminating\n", timeout.String())
|
|
go func() {
|
|
time.Sleep(timeout)
|
|
fmt.Println("Done waiting, terminating")
|
|
ic <- os.Interrupt
|
|
}()
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(tt.slackResponse))
|
|
}))
|
|
defer server.Close()
|
|
baseURL := server.Listener.Addr().String()
|
|
buff := &bytes.Buffer{}
|
|
if err := doCheck(tt.args.client, fmt.Sprintf("http://%s", baseURL), ic, 10*time.Millisecond, buff); (err != nil) != tt.wantErr {
|
|
t.Errorf("doCheck() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if len(tt.wantOut) > 0 {
|
|
for _, o := range tt.wantOut {
|
|
if !strings.Contains(buff.String(), o) {
|
|
t.Errorf("doCheck() got %s, want %s", buff.String(), o)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultProvider_Provide(t *testing.T) {
|
|
type fields struct {
|
|
provider ConfigProvider
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
want Client
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "not in cluster",
|
|
fields: fields{provider: &InClusterProvider{}},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "dummy config",
|
|
fields: fields{provider: &dummyProvider{}},
|
|
want: &kubernetes.Clientset{
|
|
DiscoveryClient: &discovery.DiscoveryClient{
|
|
LegacyPrefix: "/api",
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
d := DefaultProvider{
|
|
provider: tt.fields.provider,
|
|
}
|
|
got, err := d.Provide()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Provide() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
gotDump := litter.Sdump(got)
|
|
wantDump := litter.Sdump(tt.want)
|
|
if gotDump != wantDump {
|
|
t.Errorf("Provide() got = %v, want %v", gotDump, wantDump)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type dummyProvider struct{}
|
|
|
|
func (d dummyProvider) Provide() (*rest.Config, error) {
|
|
return &rest.Config{}, nil
|
|
}
|
|
|
|
var _ ConfigProvider = &dummyProvider{}
|
|
|
|
type brokenClientProvider struct{}
|
|
|
|
func (b brokenClientProvider) Provide() (Client, error) {
|
|
return &brokenClient{}, nil
|
|
}
|
|
|
|
var _ ClientProvider = &brokenClientProvider{}
|
|
|
|
type brokenClient struct {
|
|
batchApi batchv1.BatchV1Interface
|
|
}
|
|
|
|
func (b brokenClient) BatchV1() batchv1.BatchV1Interface {
|
|
return b.batchApi
|
|
}
|
|
|
|
var _ Client = &brokenClient{}
|
|
|
|
type batchApi struct {
|
|
cronApi batchv1.CronJobInterface
|
|
}
|
|
|
|
func (b batchApi) RESTClient() rest.Interface {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (b batchApi) CronJobs(namespace string) batchv1.CronJobInterface {
|
|
return b.cronApi
|
|
}
|
|
|
|
func (b batchApi) Jobs(namespace string) batchv1.JobInterface {
|
|
//TODO implement me
|
|
panic("implement me")
|
|
}
|
|
|
|
var _ batchv1.BatchV1Interface = &batchApi{}
|
|
|
|
type cronApi struct {
|
|
listFn func(ctx context.Context, opts v1.ListOptions) (*cronjobv1.CronJobList, error)
|
|
}
|
|
|
|
func (c cronApi) List(ctx context.Context, opts v1.ListOptions) (*cronjobv1.CronJobList, error) {
|
|
return c.listFn(ctx, opts)
|
|
}
|
|
|
|
func (c cronApi) Create(ctx context.Context, cronJob *cronjobv1.CronJob, opts v1.CreateOptions) (*cronjobv1.CronJob, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) Update(ctx context.Context, cronJob *cronjobv1.CronJob, opts v1.UpdateOptions) (*cronjobv1.CronJob, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) UpdateStatus(ctx context.Context, cronJob *cronjobv1.CronJob, opts v1.UpdateOptions) (*cronjobv1.CronJob, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) Get(ctx context.Context, name string, opts v1.GetOptions) (*cronjobv1.CronJob, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *cronjobv1.CronJob, err error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) Apply(ctx context.Context, cronJob *applyv1.CronJobApplyConfiguration, opts v1.ApplyOptions) (result *cronjobv1.CronJob, err error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (c cronApi) ApplyStatus(ctx context.Context, cronJob *applyv1.CronJobApplyConfiguration, opts v1.ApplyOptions) (result *cronjobv1.CronJob, err error) {
|
|
panic("implement me")
|
|
}
|
|
|
|
var _ batchv1.CronJobInterface = &cronApi{}
|
|
|
|
func boolP(b bool) *bool {
|
|
return &b
|
|
}
|