Files
41536738c8 refactor(cd): hard-fork - rename CRD API group argoproj.io -> deploy.hanzo.ai (#1)
* 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]>
2026-07-29 18:27:58 -07:00

457 lines
16 KiB
Go

package commands
import (
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/hanzoai/deploy/cmd/argocd/commands/headless"
"github.com/hanzoai/deploy/cmd/argocd/commands/utils"
argocdclient "github.com/hanzoai/deploy/pkg/apiclient"
projectpkg "github.com/hanzoai/deploy/pkg/apiclient/project"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/util/errors"
utilio "github.com/hanzoai/deploy/util/io"
)
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
roleCommand := &cobra.Command{
Use: "windows",
Short: "Manage a project's sync windows",
Example: `
#Add a sync window to a project
argocd proj windows add my-project \
--schedule "0 0 * * 1-5" \
--duration 3600 \
--prune
#Delete a sync window from a project
argocd proj windows delete <project-name> <window-id>
#List project sync windows
argocd proj windows list <project-name>`,
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
}
roleCommand.AddCommand(NewProjectWindowsDisableManualSyncCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsEnableManualSyncCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsDisableSyncOverrunCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsEnableSyncOverrunCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsAddWindowCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsDeleteCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsListCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsUpdateCommand(clientOpts))
return roleCommand
}
// newProjectWindowsToggleCommand creates a command for toggling a boolean field on a sync window
func newProjectWindowsToggleCommand(clientOpts *argocdclient.ClientOptions, use, short, long, example string, updateFn func(*v1alpha1.SyncWindow)) *cobra.Command {
return &cobra.Command{
Use: use,
Short: short,
Long: long,
Example: example,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
found := false
for i, window := range proj.Spec.SyncWindows {
if id == i {
updateFn(window)
found = true
break
}
}
if !found {
errors.CheckError(fmt.Errorf("window with id '%d' not found", id))
}
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
}
// NewProjectWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"disable-manual-sync PROJECT ID",
"Disable manual sync for a sync window",
"Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Disable manual sync for a sync window for the Project
argocd proj windows disable-manual-sync PROJECT ID
#Disabling manual sync for a windows set on the default project with Id 0
argocd proj windows disable-manual-sync default 0`,
func(window *v1alpha1.SyncWindow) {
window.ManualSync = false
},
)
}
// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `argocd proj windows enable-manual-sync` command
func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"enable-manual-sync PROJECT ID",
"Enable manual sync for a sync window",
"Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Enabling manual sync for a general case
argocd proj windows enable-manual-sync PROJECT ID
#Enabling manual sync for a windows set on the default project with Id 2
argocd proj windows enable-manual-sync default 2
#Enabling manual sync with a custom message
argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin`,
func(window *v1alpha1.SyncWindow) {
window.ManualSync = true
},
)
}
// NewProjectWindowsDisableSyncOverrunCommand returns a new instance of an `argocd proj windows disable-sync-overrun` command
func NewProjectWindowsDisableSyncOverrunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"disable-sync-overrun PROJECT ID",
"Disable sync overrun for a sync window",
"Disable sync overrun for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Disable sync overrun for a sync window for the Project
argocd proj windows disable-sync-overrun PROJECT ID
#Disabling sync overrun for a window set on the default project with Id 0
argocd proj windows disable-sync-overrun default 0`,
func(window *v1alpha1.SyncWindow) {
window.SyncOverrun = false
},
)
}
// NewProjectWindowsEnableSyncOverrunCommand returns a new instance of an `argocd proj windows enable-sync-overrun` command
func NewProjectWindowsEnableSyncOverrunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return newProjectWindowsToggleCommand(
clientOpts,
"enable-sync-overrun PROJECT ID",
"Enable sync overrun for a sync window",
"Enable sync overrun for a sync window. When enabled on a deny window, syncs that started before the deny window will be allowed to continue. When enabled on an allow window, syncs that started during the allow window can continue after the window ends. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
`
#Enable sync overrun for a sync window
argocd proj windows enable-sync-overrun PROJECT ID
#Enabling sync overrun for a window set on the default project with Id 2
argocd proj windows enable-sync-overrun default 2`,
func(window *v1alpha1.SyncWindow) {
window.SyncOverrun = true
},
)
}
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows add` command
func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
kind string
schedule string
duration string
applications []string
namespaces []string
clusters []string
manualSync bool
syncOverrun bool
timeZone string
andOperator bool
description string
)
command := &cobra.Command{
Use: "add PROJECT",
Short: "Add a sync window to a project",
Example: `
#Add a 1 hour allow sync window
argocd proj windows add PROJECT \
--kind allow \
--schedule "0 22 * * *" \
--duration 1h \
--applications "*" \
--description "Ticket 123"
#Add a deny sync window with the ability to manually sync and sync overrun.
argocd proj windows add PROJECT \
--kind deny \
--schedule "30 10 * * *" \
--duration 30m \
--applications "prod-\\*,website" \
--namespaces "default,\\*-prod" \
--clusters "prod,staging" \
--manual-sync \
--sync-overrun \
--description "Ticket 123"`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync, timeZone, andOperator, description, syncOverrun)
errors.CheckError(err)
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
command.Flags().StringVarP(&kind, "kind", "k", "", "Sync window kind, either allow or deny")
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
command.Flags().BoolVar(&manualSync, "manual-sync", false, "Allow manual syncs for both deny and allow windows")
command.Flags().BoolVar(&syncOverrun, "sync-overrun", false, "Allow syncs to continue: for deny windows, syncs that started before the window; for allow windows, syncs that started during the window")
command.Flags().StringVar(&timeZone, "time-zone", "UTC", "Time zone of the sync window")
command.Flags().BoolVar(&andOperator, "use-and-operator", false, "Use AND operator for matching applications, namespaces and clusters instead of the default OR operator")
command.Flags().StringVar(&description, "description", "", `Sync window description`)
return command
}
// NewProjectWindowsDeleteCommand returns a new instance of an `argocd proj windows delete` command
func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "delete PROJECT ID",
Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Example: `
#Delete a sync window from a project (default) with ID 0
argocd proj windows delete default 0
#Delete a sync window from a project (new-project) with ID 1
argocd proj windows delete new-project 1`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
err = proj.Spec.DeleteWindow(id)
errors.CheckError(err)
promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
canDelete := promptUtil.Confirm("Are you sure you want to delete sync window? [y/n]")
if canDelete {
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
} else {
fmt.Print("The command to delete the sync window was cancelled\n")
}
},
}
return command
}
// NewProjectWindowsUpdateCommand returns a new instance of an `argocd proj windows update` command
func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
schedule string
duration string
applications []string
namespaces []string
clusters []string
timeZone string
description string
)
command := &cobra.Command{
Use: "update PROJECT ID",
Short: "Update a project sync window",
Long: "Update a project sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Example: `# Change a sync window's schedule
argocd proj windows update PROJECT ID \
--schedule "0 20 * * *"
`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for i, window := range proj.Spec.SyncWindows {
if id == i {
err := window.Update(schedule, duration, applications, namespaces, clusters, timeZone, description)
if err != nil {
errors.CheckError(err)
}
}
}
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
command.Flags().StringVar(&timeZone, "time-zone", "UTC", "Time zone of the sync window. (e.g. --time-zone \"America/New_York\")")
command.Flags().StringVar(&description, "description", "", "Sync window description")
return command
}
// NewProjectWindowsListCommand returns a new instance of an `argocd proj windows list` command
func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var output string
command := &cobra.Command{
Use: "list PROJECT",
Short: "List project sync windows",
Example: `
#List project windows
argocd proj windows list PROJECT
#List project windows in yaml format
argocd proj windows list PROJECT -o yaml
#List project windows info for a project name (test-project)
argocd proj windows list test-project`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(proj.Spec.SyncWindows, output, false)
errors.CheckError(err)
case "wide", "":
printSyncWindows(proj)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
// Print table of sync window data
func printSyncWindows(proj *v1alpha1.AppProject) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var fmtStr string
headers := []any{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC", "SYNCOVERRUN", "TIMEZONE", "USEANDOPERATOR"}
fmtStr = strings.Repeat("%s\t", len(headers)) + "\n"
fmt.Fprintf(w, fmtStr, headers...)
if proj.Spec.SyncWindows.HasWindows() {
for i, window := range proj.Spec.SyncWindows {
isActive, _ := window.Active()
vals := []any{
strconv.Itoa(i),
formatBoolOutput(isActive),
window.Kind,
window.Schedule,
window.Duration,
formatListOutput(window.Applications),
formatListOutput(window.Namespaces),
formatListOutput(window.Clusters),
formatBoolEnabledOutput(window.ManualSync),
formatBoolEnabledOutput(window.SyncOverrun),
window.TimeZone,
formatBoolEnabledOutput(window.UseAndOperator),
}
fmt.Fprintf(w, fmtStr, vals...)
}
}
_ = w.Flush()
}
func formatListOutput(list []string) string {
var o string
if len(list) == 0 {
o = "-"
} else {
o = strings.Join(list, ",")
}
return o
}
func formatBoolOutput(active bool) string {
var o string
if active {
o = "Active"
} else {
o = "Inactive"
}
return o
}
func formatBoolEnabledOutput(active bool) string {
var o string
if active {
o = "Enabled"
} else {
o = "Disabled"
}
return o
}