mirror of
https://github.com/hanzoai/cd.git
synced 2026-10-30 14:56:45 +00:00
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai
Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).
CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.
Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).
CGO_ENABLED=0 go build ./... verified green.
* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)
Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.
No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.
* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)
Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).
Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.
Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).
* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy
Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.
Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).
CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.
Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.
---------
Co-authored-by: Hanzo Dev <[email protected]>
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"k8s.io/client-go/util/workqueue"
|
|
|
|
hydratortypes "github.com/hanzoai/deploy/controller/hydrator/types"
|
|
"github.com/hanzoai/deploy/pkg/ratelimiter"
|
|
)
|
|
|
|
// TestNormalizeHydrationProcessors verifies that the hydration worker count is clamped to a safe minimum.
|
|
// The --hydration-processors CLI flag can be set to 0 or a negative value (the env default is clamped, but
|
|
// an explicit flag value is not), which would otherwise start zero workers and silently stall hydration.
|
|
// See https://github.com/argoproj/argo-cd/issues/27926.
|
|
func TestNormalizeHydrationProcessors(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
configured int
|
|
want int
|
|
}{
|
|
{"zero clamps to one", 0, 1},
|
|
{"negative clamps to one", -3, 1},
|
|
{"one stays one", 1, 1},
|
|
{"default preserved", 5, 5},
|
|
{"large value preserved", 50, 50},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, tc.want, normalizeHydrationProcessors(tc.configured))
|
|
})
|
|
}
|
|
}
|
|
|
|
func newTestHydrationQueue() workqueue.TypedRateLimitingInterface[hydratortypes.HydrationQueueKey] {
|
|
return workqueue.NewTypedRateLimitingQueue(
|
|
ratelimiter.NewCustomAppControllerRateLimiter[hydratortypes.HydrationQueueKey](ratelimiter.GetDefaultAppRateLimiterConfig()),
|
|
)
|
|
}
|
|
|
|
// TestHydrationQueue_DistinctKeysProcessConcurrently asserts the core feature claim of #27926: when the
|
|
// hydration queue is drained by multiple workers, distinct hydration keys are processed concurrently, while
|
|
// no single key is ever processed by two workers at the same time. The worker loop mirrors the one started
|
|
// in ApplicationController.Run.
|
|
func TestHydrationQueue_DistinctKeysProcessConcurrently(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
queue := newTestHydrationQueue()
|
|
t.Cleanup(queue.ShutDown)
|
|
|
|
const numKeys = 12
|
|
const numWorkers = 4
|
|
for i := range numKeys {
|
|
queue.Add(hydratortypes.HydrationQueueKey{SourceRepoURL: fmt.Sprintf("https://example.com/repo-%d", i)})
|
|
}
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
inFlight int
|
|
maxConcurrent int
|
|
processed int
|
|
perKeyActive = map[hydratortypes.HydrationQueueKey]bool{}
|
|
sameKeyOverlap bool
|
|
)
|
|
|
|
processNext := func() bool {
|
|
key, shutdown := queue.Get()
|
|
if shutdown {
|
|
return false
|
|
}
|
|
defer queue.Done(key)
|
|
|
|
mu.Lock()
|
|
if perKeyActive[key] {
|
|
sameKeyOverlap = true
|
|
}
|
|
perKeyActive[key] = true
|
|
inFlight++
|
|
if inFlight > maxConcurrent {
|
|
maxConcurrent = inFlight
|
|
}
|
|
mu.Unlock()
|
|
|
|
// Hold the key briefly so that concurrent processing of distinct keys is observable.
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
inFlight--
|
|
perKeyActive[key] = false
|
|
processed++
|
|
allDone := processed == numKeys
|
|
mu.Unlock()
|
|
|
|
if allDone {
|
|
queue.ShutDown()
|
|
}
|
|
return true
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for range numWorkers {
|
|
wg.Go(func() {
|
|
for processNext() {
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
assert.Equal(t, numKeys, processed, "every distinct key should be processed exactly once")
|
|
assert.False(t, sameKeyOverlap, "the same hydration key must never be processed by two workers at once")
|
|
assert.GreaterOrEqual(t, maxConcurrent, 2, "distinct hydration keys should be processed concurrently with multiple workers")
|
|
}
|
|
|
|
// TestHydrationQueue_SameKeyNotProcessedConcurrently asserts the dedup guarantee the feature relies on: a
|
|
// hydration key that is re-enqueued while it is still in-flight is withheld until the in-flight processing
|
|
// calls Done, so the same key is never handed to two workers simultaneously. See #27926.
|
|
func TestHydrationQueue_SameKeyNotProcessedConcurrently(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
queue := newTestHydrationQueue()
|
|
t.Cleanup(queue.ShutDown)
|
|
|
|
key := hydratortypes.HydrationQueueKey{SourceRepoURL: "https://example.com/repo", DestinationBranch: "env/dev"}
|
|
queue.Add(key)
|
|
|
|
started := make(chan struct{})
|
|
release := make(chan struct{})
|
|
secondPickup := make(chan hydratortypes.HydrationQueueKey, 1)
|
|
|
|
// Worker 1 takes the key and holds it (does not call Done) until released.
|
|
go func() {
|
|
k, shutdown := queue.Get()
|
|
if shutdown {
|
|
return
|
|
}
|
|
close(started)
|
|
<-release
|
|
queue.Done(k)
|
|
}()
|
|
|
|
<-started
|
|
// Re-enqueue the same key while it is still in-flight. The workqueue must not hand it to another
|
|
// worker until the first one calls Done.
|
|
queue.Add(key)
|
|
|
|
// Worker 2 attempts to take work; it must block while the key is in-flight.
|
|
go func() {
|
|
k, shutdown := queue.Get()
|
|
if !shutdown {
|
|
secondPickup <- k
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-secondPickup:
|
|
t.Fatal("the same hydration key was delivered to a second worker while still in-flight")
|
|
case <-time.After(150 * time.Millisecond):
|
|
// Expected: the re-added key is withheld until the first worker calls Done.
|
|
}
|
|
|
|
// Release the first worker; the re-added key should now become available to worker 2.
|
|
close(release)
|
|
select {
|
|
case got := <-secondPickup:
|
|
assert.Equal(t, key, got)
|
|
queue.Done(got)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("re-added key was not delivered after the first worker finished")
|
|
}
|
|
}
|