mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
go.mod still declared `module github.com/hanzoai/deploy` after the repo was renamed. A Go module path is not cosmetic — it is the resolvable location. So `go get github.com/hanzoai/cd` could not work, and anything importing this module had to spell a name that no longer exists at that address. The build log of v3.6.0 is where it surfaced: the compiler was still emitting github.com/hanzoai/deploy/cmd/cd/commands. 3541 import paths across 690 .go, 19 .proto, 4 .yaml, 3 .sh, 2 .mod, Procfile and Makefile. Plus the 62 ghcr.io/hanzoai/deploy image references, moved to ghcr.io/hanzoai/cd to match hanzo.yml's build target — a distinct string from the module path, replaced separately so neither could clobber the other. go mod tidy clean, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./... clean. Co-authored-by: Hanzo Dev <[email protected]>
228 lines
7.7 KiB
Go
228 lines
7.7 KiB
Go
package controller
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/cache"
|
|
"k8s.io/client-go/util/workqueue"
|
|
|
|
"github.com/hanzoai/cd/pkg/apis/application/v1alpha1"
|
|
"github.com/hanzoai/cd/test"
|
|
)
|
|
|
|
// drainKeys returns all keys currently queued, marking each as done.
|
|
func drainKeys(q workqueue.TypedRateLimitingInterface[string]) []string {
|
|
var keys []string
|
|
for q.Len() > 0 {
|
|
key, _ := q.Get()
|
|
keys = append(keys, key)
|
|
q.Done(key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// assertEnqueued waits until exactly one item is queued and returns it. Items added
|
|
// via AddRateLimited go through the delaying queue (1ms base delay), so polling is
|
|
// required.
|
|
func assertEnqueued(t *testing.T, q workqueue.TypedRateLimitingInterface[string]) string {
|
|
t.Helper()
|
|
require.Eventually(t, func() bool { return q.Len() >= 1 }, 2*time.Second, 5*time.Millisecond, "expected an item to be enqueued")
|
|
keys := drainKeys(q)
|
|
require.Len(t, keys, 1)
|
|
return keys[0]
|
|
}
|
|
|
|
// assertNotEnqueued asserts that nothing is queued within a short window.
|
|
func assertNotEnqueued(t *testing.T, q workqueue.TypedRateLimitingInterface[string]) {
|
|
t.Helper()
|
|
assert.Never(t, func() bool { return q.Len() > 0 }, 200*time.Millisecond, 20*time.Millisecond, "expected nothing to be enqueued")
|
|
}
|
|
|
|
func newProject(name string) *v1alpha1.AppProject {
|
|
return &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: test.FakeArgoCDNamespace}}
|
|
}
|
|
|
|
func TestAppProjectEventHandlerFuncs(t *testing.T) {
|
|
t.Run("add requeues the project and invalidates its cache entry", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
ctrl.projByNameCache.Store("my-proj", struct{}{})
|
|
h := ctrl.appProjectEventHandlerFuncs()
|
|
|
|
h.OnAdd(newProject("my-proj"), false)
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.projectRefreshQueue), "my-proj")
|
|
_, ok := ctrl.projByNameCache.Load("my-proj")
|
|
assert.False(t, ok, "project cache entry should have been invalidated")
|
|
})
|
|
|
|
t.Run("update requeues the project and invalidates its cache entry", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
ctrl.projByNameCache.Store("my-proj", struct{}{})
|
|
h := ctrl.appProjectEventHandlerFuncs()
|
|
|
|
h.OnUpdate(newProject("my-proj"), newProject("my-proj"))
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.projectRefreshQueue), "my-proj")
|
|
_, ok := ctrl.projByNameCache.Load("my-proj")
|
|
assert.False(t, ok)
|
|
})
|
|
|
|
t.Run("delete requeues the project and invalidates its cache entry", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
ctrl.projByNameCache.Store("my-proj", struct{}{})
|
|
h := ctrl.appProjectEventHandlerFuncs()
|
|
|
|
h.OnDelete(newProject("my-proj"))
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.projectRefreshQueue), "my-proj")
|
|
_, ok := ctrl.projByNameCache.Load("my-proj")
|
|
assert.False(t, ok)
|
|
})
|
|
|
|
t.Run("delete unwraps a tombstone wrapping a project", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
ctrl.projByNameCache.Store("my-proj", struct{}{})
|
|
h := ctrl.appProjectEventHandlerFuncs()
|
|
|
|
h.OnDelete(cache.DeletedFinalStateUnknown{Key: test.FakeArgoCDNamespace + "/my-proj", Obj: newProject("my-proj")})
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.projectRefreshQueue), "my-proj")
|
|
_, ok := ctrl.projByNameCache.Load("my-proj")
|
|
assert.False(t, ok)
|
|
})
|
|
|
|
t.Run("delete of a tombstone wrapping nil is ignored without panicking", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
ctrl.projByNameCache.Store("my-proj", struct{}{})
|
|
h := ctrl.appProjectEventHandlerFuncs()
|
|
|
|
assert.NotPanics(t, func() {
|
|
h.OnDelete(cache.DeletedFinalStateUnknown{Key: test.FakeArgoCDNamespace + "/gone", Obj: nil})
|
|
})
|
|
assertNotEnqueued(t, ctrl.projectRefreshQueue)
|
|
_, ok := ctrl.projByNameCache.Load("my-proj")
|
|
assert.True(t, ok, "unrelated cache entry should be untouched")
|
|
})
|
|
|
|
t.Run("unexpected object types are ignored without panicking", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.appProjectEventHandlerFuncs()
|
|
|
|
assert.NotPanics(t, func() {
|
|
h.OnAdd("not-a-project", false)
|
|
h.OnUpdate("not-a-project", "not-a-project")
|
|
h.OnDelete("not-a-project")
|
|
})
|
|
assertNotEnqueued(t, ctrl.projectRefreshQueue)
|
|
})
|
|
}
|
|
|
|
func TestApplicationEventHandlerFuncs(t *testing.T) {
|
|
t.Run("add of a processable application requeues a refresh", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
h.OnAdd(newFakeApp(), false)
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.appRefreshQueue), "my-app")
|
|
})
|
|
|
|
t.Run("update of a processable application requeues a refresh", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
old := newFakeApp()
|
|
updated := newFakeApp()
|
|
updated.ResourceVersion = "2"
|
|
h.OnUpdate(old, updated)
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.appRefreshQueue), "my-app")
|
|
assertNotEnqueued(t, ctrl.appOperationQueue)
|
|
})
|
|
|
|
t.Run("update of an application with an ongoing operation requeues an operation", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
old := newFakeApp()
|
|
updated := newFakeApp()
|
|
updated.ResourceVersion = "2"
|
|
updated.Operation = &v1alpha1.Operation{Sync: &v1alpha1.SyncOperation{}}
|
|
h.OnUpdate(old, updated)
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.appOperationQueue), "my-app")
|
|
})
|
|
|
|
t.Run("update of a terminating application requeues an operation", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
now := metav1.Now()
|
|
old := newFakeApp()
|
|
updated := newFakeApp()
|
|
updated.ResourceVersion = "2"
|
|
updated.DeletionTimestamp = &now
|
|
h.OnUpdate(old, updated)
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.appOperationQueue), "my-app")
|
|
})
|
|
|
|
t.Run("delete of a processable application requeues a refresh", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
h.OnDelete(newFakeApp())
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.appRefreshQueue), "my-app")
|
|
})
|
|
|
|
t.Run("delete unwraps a tombstone wrapping an application", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
app := newFakeApp()
|
|
h.OnDelete(cache.DeletedFinalStateUnknown{Key: app.Namespace + "/" + app.Name, Obj: app})
|
|
|
|
assert.Contains(t, assertEnqueued(t, ctrl.appRefreshQueue), "my-app")
|
|
})
|
|
|
|
t.Run("delete of a tombstone wrapping nil is ignored without panicking", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
assert.NotPanics(t, func() {
|
|
h.OnDelete(cache.DeletedFinalStateUnknown{Key: "gone", Obj: nil})
|
|
})
|
|
assertNotEnqueued(t, ctrl.appRefreshQueue)
|
|
})
|
|
|
|
t.Run("an application in a disallowed namespace is ignored", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
app := newFakeApp()
|
|
app.Namespace = "some-other-namespace"
|
|
h.OnAdd(app, false)
|
|
h.OnUpdate(app, app)
|
|
h.OnDelete(app)
|
|
|
|
assertNotEnqueued(t, ctrl.appRefreshQueue)
|
|
})
|
|
|
|
t.Run("unexpected object types are ignored without panicking", func(t *testing.T) {
|
|
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
|
|
h := ctrl.applicationEventHandlerFuncs()
|
|
|
|
assert.NotPanics(t, func() {
|
|
h.OnAdd("not-an-app", false)
|
|
h.OnUpdate("not-an-app", "not-an-app")
|
|
h.OnDelete("not-an-app")
|
|
})
|
|
assertNotEnqueued(t, ctrl.appRefreshQueue)
|
|
})
|
|
}
|