Files
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

263 lines
9.3 KiB
Go

package commands
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"text/tabwriter"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"sigs.k8s.io/yaml"
"github.com/hanzoai/cd/cmd/cd/commands/headless"
"github.com/hanzoai/cd/cmd/util"
cdclient "github.com/hanzoai/cd/pkg/apiclient"
applicationpkg "github.com/hanzoai/cd/pkg/apiclient/application"
"github.com/hanzoai/cd/pkg/apis/application"
"github.com/hanzoai/cd/pkg/apis/application/v1alpha1"
"github.com/hanzoai/cd/util/cd"
"github.com/hanzoai/cd/util/errors"
"github.com/hanzoai/cd/util/grpc"
utilio "github.com/hanzoai/cd/util/io"
"github.com/hanzoai/cd/util/templates"
)
type DisplayedAction struct {
Group string
Kind string
Name string
Action string
Disabled bool
}
var appActionExample = templates.Examples(`
# List all the available actions for an application
cd app actions list APPNAME
# Run an available action for an application
cd app actions run APPNAME ACTION --kind KIND [--resource-name RESOURCE] [--namespace NAMESPACE] [--group GROUP]
`)
// NewApplicationResourceActionsCommand returns a new instance of an `cd app actions` command
func NewApplicationResourceActionsCommand(clientOpts *cdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "actions",
Short: "Manage Resource actions",
Example: appActionExample,
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
}
command.AddCommand(NewApplicationResourceActionsListCommand(clientOpts))
command.AddCommand(NewApplicationResourceActionsRunCommand(clientOpts))
return command
}
// NewApplicationResourceActionsListCommand returns a new instance of an `cd app actions list` command
func NewApplicationResourceActionsListCommand(clientOpts *cdclient.ClientOptions) *cobra.Command {
var namespace string
var kind string
var group string
var resourceName string
var output string
var appNamespace string
command := &cobra.Command{
Use: "list APPNAME",
Short: "Lists available actions on a resource",
Example: templates.Examples(`
# List all the available actions for an application
cd app actions list APPNAME
`),
}
command.Run = func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName, appNs := cd.ParseFromQualifiedName(args[0], appNamespace)
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
resources, err := getActionableResourcesForApplication(ctx, appIf, &appNs, &appName)
errors.CheckError(err)
filteredObjects, err := util.FilterResources(command.Flags().Changed("group"), resources, group, kind, namespace, resourceName, true)
errors.CheckError(err)
var availableActions []DisplayedAction
for i := range filteredObjects {
obj := filteredObjects[i]
gvk := obj.GroupVersionKind()
availActionsForResource, err := appIf.ListResourceActions(ctx, &applicationpkg.ApplicationResourceRequest{
Name: &appName,
AppNamespace: &appNs,
Namespace: new(obj.GetNamespace()),
ResourceName: new(obj.GetName()),
Group: new(gvk.Group),
Kind: new(gvk.Kind),
Version: new(gvk.Version),
})
errors.CheckError(err)
for _, action := range availActionsForResource.Actions {
displayAction := DisplayedAction{
Group: gvk.Group,
Kind: gvk.Kind,
Name: obj.GetName(),
Action: action.Name,
Disabled: action.Disabled,
}
availableActions = append(availableActions, displayAction)
}
}
switch output {
case "yaml":
yamlBytes, err := yaml.Marshal(availableActions)
errors.CheckError(err)
fmt.Println(string(yamlBytes))
case "json":
jsonBytes, err := json.MarshalIndent(availableActions, "", " ")
errors.CheckError(err)
fmt.Println(string(jsonBytes))
case "":
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprint(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
for _, action := range availableActions {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", action.Group, action.Kind, action.Name, action.Action, strconv.FormatBool(action.Disabled))
}
_ = w.Flush()
}
}
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the application")
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
command.Flags().StringVar(&kind, "kind", "", "Kind")
command.Flags().StringVar(&group, "group", "", "Group")
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
command.Flags().StringVarP(&output, "out", "o", "", "Output format. One of: yaml, json")
return command
}
// NewApplicationResourceActionsRunCommand returns a new instance of an `cd app actions run` command
func NewApplicationResourceActionsRunCommand(clientOpts *cdclient.ClientOptions) *cobra.Command {
var namespace string
var appNamespace string
var resourceName string
var kind string
var group string
var all bool
command := &cobra.Command{
Use: "run APPNAME ACTION",
Short: "Runs an available action on resource(s) matching the specified filters.",
Long: "All filters except --kind are optional. Use --all to run the action on all matching resources if more than one resource matches the filters. Actions may only be run on resources that are represented in git and cannot be run on child resources.",
Example: templates.Examples(`
# Run an available action for an application
cd app actions run APPNAME ACTION --kind KIND [--resource-name RESOURCE] [--namespace NAMESPACE] [--group GROUP]
`),
}
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource on which the action should be run")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the application")
command.Flags().StringVar(&namespace, "namespace", "", "Namespace of the resource on which the action should be run")
command.Flags().StringVar(&kind, "kind", "", "Kind of the resource on which the action should be run")
command.Flags().StringVar(&group, "group", "", "Group of the resource on which the action should be run")
errors.CheckError(command.MarkFlagRequired("kind"))
command.Flags().BoolVar(&all, "all", false, "Indicates whether to run the action on multiple matching resources")
command.Run = func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName, appNs := cd.ParseFromQualifiedName(args[0], appNamespace)
actionName := args[1]
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
defer utilio.Close(conn)
resources, err := getActionableResourcesForApplication(ctx, appIf, &appNs, &appName)
errors.CheckError(err)
filteredObjects, err := util.FilterResources(command.Flags().Changed("group"), resources, group, kind, namespace, resourceName, all)
errors.CheckError(err)
resGroup := filteredObjects[0].GroupVersionKind().Group
for i := range filteredObjects[1:] {
if filteredObjects[i].GroupVersionKind().Group != resGroup {
log.Fatal("Ambiguous resource group. Use flag --group to specify resource group explicitly.")
}
}
for i := range filteredObjects {
obj := filteredObjects[i]
gvk := obj.GroupVersionKind()
objResourceName := obj.GetName()
_, err := appIf.RunResourceActionV2(ctx, &applicationpkg.ResourceActionRunRequestV2{
Name: &appName,
AppNamespace: &appNs,
Namespace: new(obj.GetNamespace()),
ResourceName: new(objResourceName),
Group: new(gvk.Group),
Kind: new(gvk.Kind),
Version: new(gvk.GroupVersion().Version),
Action: new(actionName),
// TODO: add support for parameters
})
if err == nil {
continue
}
if grpc.UnwrapGRPCStatus(err).Code() != codes.Unimplemented {
errors.CheckError(err)
}
fmt.Println("RunResourceActionV2 is not supported by the server, falling back to RunResourceAction.")
//nolint:staticcheck // RunResourceAction is deprecated, but we still need to support it for backward compatibility.
_, err = appIf.RunResourceAction(ctx, &applicationpkg.ResourceActionRunRequest{
Name: &appName,
AppNamespace: &appNs,
Namespace: new(obj.GetNamespace()),
ResourceName: new(objResourceName),
Group: new(gvk.Group),
Kind: new(gvk.Kind),
Version: new(gvk.GroupVersion().Version),
Action: new(actionName),
})
errors.CheckError(err)
}
}
return command
}
func getActionableResourcesForApplication(ctx context.Context, appIf applicationpkg.ApplicationServiceClient, appNs *string, appName *string) ([]*v1alpha1.ResourceDiff, error) {
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{
ApplicationName: appName,
AppNamespace: appNs,
})
if err != nil {
return nil, err
}
app, err := appIf.Get(ctx, &applicationpkg.ApplicationQuery{
Name: appName,
AppNamespace: appNs,
})
if err != nil {
return nil, err
}
app.Kind = application.ApplicationKind
app.APIVersion = "apps.hanzo.ai/v1alpha1"
appManifest, err := json.Marshal(app)
if err != nil {
return nil, err
}
appGVK := app.GroupVersionKind()
return append(resources.Items, &v1alpha1.ResourceDiff{
Group: appGVK.Group,
Kind: appGVK.Kind,
Namespace: app.Namespace,
Name: *appName,
LiveState: string(appManifest),
}), nil
}