215 lines
6.1 KiB
Go
215 lines
6.1 KiB
Go
package gc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1"
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
// listProvider serves a canned instance list and records deletions.
|
|
type listProvider struct {
|
|
mu sync.Mutex
|
|
instances []provider.Instance
|
|
listErr error
|
|
deleted []string
|
|
}
|
|
|
|
func (l *listProvider) Create(context.Context, provider.CreateRequest) (string, error) {
|
|
return "", errors.New("not used")
|
|
}
|
|
|
|
func (l *listProvider) Get(context.Context, string) (*provider.Instance, error) {
|
|
return nil, provider.ErrNotFound
|
|
}
|
|
|
|
func (l *listProvider) Delete(_ context.Context, id string) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.deleted = append(l.deleted, id)
|
|
return nil
|
|
}
|
|
|
|
func (l *listProvider) ListByTag(context.Context) ([]provider.Instance, error) {
|
|
return l.instances, l.listErr
|
|
}
|
|
|
|
func (l *listProvider) deletedIDs() []string {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return append([]string(nil), l.deleted...)
|
|
}
|
|
|
|
func proxyWithUID(name, uid string, mut ...func(*crawlv1alpha1.Proxy)) *crawlv1alpha1.Proxy {
|
|
p := &crawlv1alpha1.Proxy{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default", UID: types.UID(uid)},
|
|
Spec: crawlv1alpha1.ProxySpec{
|
|
Mode: crawlv1alpha1.ModeManaged,
|
|
Provider: "stub",
|
|
},
|
|
}
|
|
for _, m := range mut {
|
|
m(p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func newReader(t *testing.T, objs ...client.Object) client.Reader {
|
|
t.Helper()
|
|
s := runtime.NewScheme()
|
|
if err := crawlv1alpha1.AddToScheme(s); err != nil {
|
|
t.Fatalf("scheme: %v", err)
|
|
}
|
|
return fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build()
|
|
}
|
|
|
|
func oldInstance(id, uid string) provider.Instance {
|
|
return provider.Instance{ID: id, UID: uid, State: provider.StateRunning,
|
|
CreatedAt: time.Now().Add(-time.Hour)}
|
|
}
|
|
|
|
func newSweeper(reader client.Reader, providers map[string]provider.Provider) *Sweeper {
|
|
return &Sweeper{
|
|
Reader: reader,
|
|
Providers: providers,
|
|
Interval: 10 * time.Minute,
|
|
MinAge: 10 * time.Minute,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
func TestSweep_deletesOnlyTrueOrphans(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
deletingCR := proxyWithUID("deleting", "uid-deleting", func(p *crawlv1alpha1.Proxy) {
|
|
now := metav1.Now()
|
|
p.DeletionTimestamp = &now
|
|
p.Finalizers = []string{crawlv1alpha1.FinalizerName}
|
|
})
|
|
prov := &listProvider{instances: []provider.Instance{
|
|
oldInstance("inst-live", "uid-live"),
|
|
oldInstance("inst-orphan", "uid-orphan"),
|
|
oldInstance("inst-deleting", "uid-deleting"),
|
|
{ID: "inst-young", UID: "uid-young-orphan", State: provider.StateRunning,
|
|
CreatedAt: time.Now().Add(-time.Minute)},
|
|
oldInstance("inst-unlabelled", ""),
|
|
}}
|
|
s := newSweeper(
|
|
newReader(t, proxyWithUID("live", "uid-live"), deletingCR),
|
|
map[string]provider.Provider{"stub": prov},
|
|
)
|
|
|
|
s.sweep(context.Background())
|
|
|
|
got := prov.deletedIDs()
|
|
if len(got) != 1 || got[0] != "inst-orphan" {
|
|
t.Errorf("deleted %v, want exactly [inst-orphan]:\n"+
|
|
"live CR's instance must stay; a deleting CR still owns its instance (finalizer, not GC);\n"+
|
|
"young instances may be mid-create; unlabelled instances have no ownership proof", got)
|
|
}
|
|
}
|
|
|
|
func TestSweep_providerErrorDoesNotAbortOthers(t *testing.T) {
|
|
t.Parallel()
|
|
broken := &listProvider{listErr: errors.New("cloud is down")}
|
|
working := &listProvider{instances: []provider.Instance{oldInstance("inst-orphan", "uid-orphan")}}
|
|
s := newSweeper(newReader(t), map[string]provider.Provider{
|
|
"broken": broken,
|
|
"working": working,
|
|
})
|
|
|
|
s.sweep(context.Background())
|
|
|
|
if got := working.deletedIDs(); len(got) != 1 {
|
|
t.Errorf("working provider deletions = %v, want the orphan despite the broken provider", got)
|
|
}
|
|
}
|
|
|
|
// errReader fails every List: without the live set nothing can be proven
|
|
// orphaned, so the sweep must delete nothing.
|
|
type errReader struct{ client.Reader }
|
|
|
|
func (errReader) List(context.Context, client.ObjectList, ...client.ListOption) error {
|
|
return errors.New("cache broken")
|
|
}
|
|
|
|
func TestSweep_listFailureSkipsSweep(t *testing.T) {
|
|
t.Parallel()
|
|
prov := &listProvider{instances: []provider.Instance{oldInstance("inst-orphan", "uid-orphan")}}
|
|
s := newSweeper(errReader{}, map[string]provider.Provider{"stub": prov})
|
|
|
|
s.sweep(context.Background())
|
|
|
|
if got := prov.deletedIDs(); len(got) != 0 {
|
|
t.Errorf("deleted %v with an unreadable live set, want nothing", got)
|
|
}
|
|
}
|
|
|
|
func TestStart_namespaceGuard(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newSweeper(newReader(t), nil)
|
|
s.NamespaceRestricted = true
|
|
err := s.Start(context.Background())
|
|
if err == nil || !strings.Contains(err.Error(), "--gc-allow-namespaced") {
|
|
t.Errorf("Start with restricted cache = %v, want refusal naming the override flag", err)
|
|
}
|
|
|
|
s2 := newSweeper(newReader(t), nil)
|
|
s2.NamespaceRestricted = true
|
|
s2.AllowNamespaced = true
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() { done <- s2.Start(ctx) }()
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Errorf("Start with override = %v, want it to run until cancel", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Start did not stop on cancel")
|
|
}
|
|
}
|
|
|
|
func TestStart_sweepsOnIntervalAndStops(t *testing.T) {
|
|
t.Parallel()
|
|
prov := &listProvider{instances: []provider.Instance{oldInstance("inst-orphan", "uid-orphan")}}
|
|
s := newSweeper(newReader(t), map[string]provider.Provider{"stub": prov})
|
|
s.Interval = 5 * time.Millisecond
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() { done <- s.Start(ctx) }()
|
|
|
|
deadline := time.After(5 * time.Second)
|
|
for len(prov.deletedIDs()) == 0 {
|
|
select {
|
|
case <-deadline:
|
|
t.Fatal("no sweep ran")
|
|
case <-time.After(2 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Errorf("Start = %v, want nil", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Start did not stop on cancel")
|
|
}
|
|
}
|