Files
deploy/cmd/cd/commands/projectwindows.go
zeekayandHanzo Dev 1ab6e799c1 cd: the module is github.com/hanzoai/cd, where it is actually hosted
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]>
2026-07-28 21:16:24 -07:00

457 lines
16 KiB
Go

package commands
import (
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/hanzoai/cd/cmd/cd/commands/headless"
"github.com/hanzoai/cd/cmd/cd/commands/utils"
cdclient "github.com/hanzoai/cd/pkg/apiclient"
projectpkg "github.com/hanzoai/cd/pkg/apiclient/project"
"github.com/hanzoai/cd/pkg/apis/application/v1alpha1"
"github.com/hanzoai/cd/util/errors"
utilio "github.com/hanzoai/cd/util/io"
)
// NewProjectWindowsCommand returns a new instance of the `cd proj windows` command
func NewProjectWindowsCommand(clientOpts *cdclient.ClientOptions) *cobra.Command {
roleCommand := &cobra.Command{
Use: "windows",
Short: "Manage a project's sync windows",
Example: `
#Add a sync window to a project
cd proj windows add my-project \
--schedule "0 0 * * 1-5" \
--duration 3600 \
--prune
#Delete a sync window from a project
cd proj windows delete <project-name> <window-id>
#List project sync windows
cd 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 *cdclient.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 `cd proj windows disable-manual-sync` command
func NewProjectWindowsDisableManualSyncCommand(clientOpts *cdclient.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 \"cd proj windows list PROJECT\"",
`
#Disable manual sync for a sync window for the Project
cd proj windows disable-manual-sync PROJECT ID
#Disabling manual sync for a windows set on the default project with Id 0
cd proj windows disable-manual-sync default 0`,
func(window *v1alpha1.SyncWindow) {
window.ManualSync = false
},
)
}
// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `cd proj windows enable-manual-sync` command
func NewProjectWindowsEnableManualSyncCommand(clientOpts *cdclient.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 \"cd proj windows list PROJECT\"",
`
#Enabling manual sync for a general case
cd proj windows enable-manual-sync PROJECT ID
#Enabling manual sync for a windows set on the default project with Id 2
cd proj windows enable-manual-sync default 2
#Enabling manual sync with a custom message
cd 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 `cd proj windows disable-sync-overrun` command
func NewProjectWindowsDisableSyncOverrunCommand(clientOpts *cdclient.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 \"cd proj windows list PROJECT\"",
`
#Disable sync overrun for a sync window for the Project
cd proj windows disable-sync-overrun PROJECT ID
#Disabling sync overrun for a window set on the default project with Id 0
cd proj windows disable-sync-overrun default 0`,
func(window *v1alpha1.SyncWindow) {
window.SyncOverrun = false
},
)
}
// NewProjectWindowsEnableSyncOverrunCommand returns a new instance of an `cd proj windows enable-sync-overrun` command
func NewProjectWindowsEnableSyncOverrunCommand(clientOpts *cdclient.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 \"cd proj windows list PROJECT\"",
`
#Enable sync overrun for a sync window
cd proj windows enable-sync-overrun PROJECT ID
#Enabling sync overrun for a window set on the default project with Id 2
cd proj windows enable-sync-overrun default 2`,
func(window *v1alpha1.SyncWindow) {
window.SyncOverrun = true
},
)
}
// NewProjectWindowsAddWindowCommand returns a new instance of an `cd proj windows add` command
func NewProjectWindowsAddWindowCommand(clientOpts *cdclient.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
cd 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.
cd 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 `cd proj windows delete` command
func NewProjectWindowsDeleteCommand(clientOpts *cdclient.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 \"cd proj windows list PROJECT\"",
Example: `
#Delete a sync window from a project (default) with ID 0
cd proj windows delete default 0
#Delete a sync window from a project (new-project) with ID 1
cd 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 `cd proj windows update` command
func NewProjectWindowsUpdateCommand(clientOpts *cdclient.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 \"cd proj windows list PROJECT\"",
Example: `# Change a sync window's schedule
cd 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 `cd proj windows list` command
func NewProjectWindowsListCommand(clientOpts *cdclient.ClientOptions) *cobra.Command {
var output string
command := &cobra.Command{
Use: "list PROJECT",
Short: "List project sync windows",
Example: `
#List project windows
cd proj windows list PROJECT
#List project windows in yaml format
cd proj windows list PROJECT -o yaml
#List project windows info for a project name (test-project)
cd 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
}