367 lines
11 KiB
Go
367 lines
11 KiB
Go
package cmd
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"text/tabwriter"
|
||
|
||
"kmanage/pkg/git"
|
||
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
var gitWebhookCmd = &cobra.Command{
|
||
Use: "webhook",
|
||
Aliases: []string{"webhooks", "hook", "hooks"},
|
||
Short: "Manage Git webhooks",
|
||
Long: `Commands to list, delete, and prune webhooks in Git repositories.`,
|
||
}
|
||
|
||
var gitWebhookListCmd = &cobra.Command{
|
||
Use: "list <owner/repo>",
|
||
Short: "List all webhooks configured for a specific repository",
|
||
Args: cobra.ExactArgs(1),
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
owner, repo, err := parseRepoArg(args[0])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
accountFlag, _ := cmd.Flags().GetString("account")
|
||
client, err := getGitClientForAccount(accountFlag)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
hooks, err := client.ListHooks(owner, repo)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to retrieve webhooks for %s/%s (%s): %w", owner, repo, client.AccountName, err)
|
||
}
|
||
|
||
if len(hooks) == 0 {
|
||
fmt.Printf("No webhooks found in repository %s/%s (%s).\n", owner, repo, client.AccountName)
|
||
return nil
|
||
}
|
||
|
||
noHeader, _ := cmd.Flags().GetBool("no-header")
|
||
if !noHeader {
|
||
fmt.Printf("Webhooks for %s/%s (Account: %s):\n", owner, repo, client.AccountName)
|
||
}
|
||
|
||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||
if !noHeader {
|
||
fmt.Fprintln(w, "ID\tTYPE\tSTATUS\tEVENTS\tTARGET URL")
|
||
fmt.Fprintln(w, "--\t----\t------\t------\t----------")
|
||
}
|
||
|
||
for _, h := range hooks {
|
||
status := "active"
|
||
if !h.Active {
|
||
status = "inactive"
|
||
}
|
||
events := strings.Join(h.Events, ",")
|
||
if events == "" {
|
||
events = "-"
|
||
}
|
||
hookType := h.Type
|
||
if hookType == "" {
|
||
hookType = h.Name
|
||
}
|
||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", h.ID, hookType, status, events, h.TargetURL())
|
||
}
|
||
|
||
return w.Flush()
|
||
},
|
||
}
|
||
|
||
var gitWebhookDeleteCmd = &cobra.Command{
|
||
Use: "delete <owner/repo>",
|
||
Aliases: []string{"del", "remove", "rm"},
|
||
Short: "Delete webhooks from a repository",
|
||
Long: `Deletes webhooks from a repository by ID, URL filter, or all at once.
|
||
|
||
Examples:
|
||
kmanage git webhook delete Docker/authentik --id 12
|
||
kmanage git webhook delete Docker/authentik --all
|
||
kmanage git webhook delete Docker/authentik --url kmd.example.com`,
|
||
Args: cobra.ExactArgs(1),
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
owner, repo, err := parseRepoArg(args[0])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
hookID, _ := cmd.Flags().GetInt64("id")
|
||
deleteAll, _ := cmd.Flags().GetBool("all")
|
||
urlFilter, _ := cmd.Flags().GetString("url")
|
||
autoYes, _ := cmd.Flags().GetBool("yes")
|
||
accountFlag, _ := cmd.Flags().GetString("account")
|
||
|
||
if hookID == 0 && !deleteAll && urlFilter == "" {
|
||
_ = cmd.Help()
|
||
fmt.Fprintln(os.Stderr)
|
||
return fmt.Errorf("❌ Error: Specify what to delete:\n --id <id> (single webhook)\n --all (all webhooks in repo)\n --url <url> (webhooks matching target URL)")
|
||
}
|
||
|
||
client, err := getGitClientForAccount(accountFlag)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if hookID != 0 {
|
||
if !autoYes {
|
||
if !confirmAction(fmt.Sprintf("Do you really want to delete webhook #%d in %s/%s?", hookID, owner, repo)) {
|
||
fmt.Println("Cancelled.")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
err := client.DeleteHook(owner, repo, hookID)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to delete webhook #%d: %w", hookID, err)
|
||
}
|
||
fmt.Printf("✅ Webhook #%d in %s/%s successfully deleted.\n", hookID, owner, repo)
|
||
return nil
|
||
}
|
||
|
||
hooks, err := client.ListHooks(owner, repo)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to retrieve webhooks for %s/%s: %w", owner, repo, err)
|
||
}
|
||
|
||
var targets []git.Hook
|
||
for _, h := range hooks {
|
||
if deleteAll {
|
||
targets = append(targets, h)
|
||
} else if urlFilter != "" && strings.Contains(h.TargetURL(), urlFilter) {
|
||
targets = append(targets, h)
|
||
}
|
||
}
|
||
|
||
if len(targets) == 0 {
|
||
fmt.Println("No matching webhooks found to delete.")
|
||
return nil
|
||
}
|
||
|
||
fmt.Printf("The following webhooks in %s/%s (%s) will be deleted:\n", owner, repo, client.AccountName)
|
||
for _, t := range targets {
|
||
fmt.Printf(" - ID %d: %s\n", t.ID, t.TargetURL())
|
||
}
|
||
|
||
if !autoYes {
|
||
if !confirmAction(fmt.Sprintf("Confirm deletion of %d webhook(s)?", len(targets))) {
|
||
fmt.Println("Cancelled.")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
successCount := 0
|
||
for _, t := range targets {
|
||
if err := client.DeleteHook(owner, repo, t.ID); err != nil {
|
||
fmt.Printf("❌ Failed to delete webhook #%d (%s): %v\n", t.ID, t.TargetURL(), err)
|
||
} else {
|
||
fmt.Printf("✅ Deleted webhook #%d (%s)\n", t.ID, t.TargetURL())
|
||
successCount++
|
||
}
|
||
}
|
||
|
||
fmt.Printf("\nSuccessfully deleted %d of %d webhook(s).\n", successCount, len(targets))
|
||
return nil
|
||
},
|
||
}
|
||
|
||
type matchedHook struct {
|
||
Account string
|
||
Owner string
|
||
Repo string
|
||
HookID int64
|
||
URL string
|
||
Type string
|
||
Active bool
|
||
RepoRef string
|
||
Client *git.Client
|
||
}
|
||
|
||
var gitWebhookPruneCmd = &cobra.Command{
|
||
Use: "prune [flags]",
|
||
Aliases: []string{"clean", "cleanup", "delete-all", "remove-all"},
|
||
Short: "Delete webhooks matching a specific target URL across repositories",
|
||
Long: `Searches repositories for webhooks with the given target URL and batch deletes them.
|
||
Supports pruning within the default account, a specific account, or across all configured accounts using '--all-accounts'.
|
||
|
||
Examples:
|
||
# Preview matching webhooks without deleting (Dry-Run):
|
||
kmanage git webhook prune --url kmd.example.com --dry-run
|
||
|
||
# Prune matching webhooks in default account:
|
||
kmanage git webhook prune --url kmd.example.com
|
||
|
||
# Prune matching webhooks across ALL configured accounts:
|
||
kmanage git webhook prune --url kmd.example.com --all-accounts -y`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
urlFilter, _ := cmd.Flags().GetString("url")
|
||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||
autoYes, _ := cmd.Flags().GetBool("yes")
|
||
accountFlag, _ := cmd.Flags().GetString("account")
|
||
allAccountsFlag, _ := cmd.Flags().GetBool("all-accounts")
|
||
|
||
if urlFilter == "" {
|
||
_ = cmd.Help()
|
||
fmt.Fprintln(os.Stderr)
|
||
return fmt.Errorf("❌ Error: The flag '--url' (or '-u') is required")
|
||
}
|
||
|
||
clients, err := getSelectedGitClients(accountFlag, allAccountsFlag)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
fmt.Printf("🔍 Searching repositories for webhooks matching '%s'...\n", urlFilter)
|
||
|
||
var matches []matchedHook
|
||
for _, client := range clients {
|
||
reposWithHooks, err := client.ListRepositoriesWithHooks()
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "⚠️ Warning searching account '%s': %v\n", client.AccountName, err)
|
||
continue
|
||
}
|
||
|
||
for _, r := range reposWithHooks {
|
||
owner, repo, err := parseRepoArg(r.FullName)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
for _, h := range r.Hooks {
|
||
if strings.Contains(h.TargetURL(), urlFilter) {
|
||
matches = append(matches, matchedHook{
|
||
Account: client.AccountName,
|
||
Owner: owner,
|
||
Repo: repo,
|
||
HookID: h.ID,
|
||
URL: h.TargetURL(),
|
||
Type: h.Type,
|
||
Active: h.Active,
|
||
RepoRef: r.FullName,
|
||
Client: client,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(matches) == 0 {
|
||
fmt.Printf("No webhooks found matching URL filter '%s'.\n", urlFilter)
|
||
return nil
|
||
}
|
||
|
||
noHeader, _ := cmd.Flags().GetBool("no-header")
|
||
if !noHeader {
|
||
fmt.Printf("\nMatching Webhooks (%d):\n", len(matches))
|
||
}
|
||
|
||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||
if !noHeader {
|
||
if len(clients) > 1 || allAccountsFlag {
|
||
fmt.Fprintln(w, "ACCOUNT\tREPOSITORY\tID\tTYPE\tSTATUS\tTARGET URL")
|
||
fmt.Fprintln(w, "-------\t----------\t--\t----\t------\t----------")
|
||
} else {
|
||
fmt.Fprintln(w, "REPOSITORY\tID\tTYPE\tSTATUS\tTARGET URL")
|
||
fmt.Fprintln(w, "----------\t--\t----\t------\t----------")
|
||
}
|
||
}
|
||
|
||
for _, m := range matches {
|
||
status := "active"
|
||
if !m.Active {
|
||
status = "inactive"
|
||
}
|
||
hookType := m.Type
|
||
if hookType == "" {
|
||
hookType = "-"
|
||
}
|
||
if len(clients) > 1 || allAccountsFlag {
|
||
fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\t%s\n", m.Account, m.RepoRef, m.HookID, hookType, status, m.URL)
|
||
} else {
|
||
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", m.RepoRef, m.HookID, hookType, status, m.URL)
|
||
}
|
||
}
|
||
_ = w.Flush()
|
||
fmt.Println()
|
||
|
||
if dryRun {
|
||
fmt.Println("ℹ️ Dry-run mode active. No webhooks were deleted.")
|
||
return nil
|
||
}
|
||
|
||
if !autoYes {
|
||
prompt := fmt.Sprintf("Do you really want to delete all %d matching webhook(s)?", len(matches))
|
||
if !confirmAction(prompt) {
|
||
fmt.Println("Cancelled.")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
fmt.Println("\nDeleting webhooks...")
|
||
|
||
var wg sync.WaitGroup
|
||
semaphore := make(chan struct{}, 8)
|
||
var successCount int
|
||
var countMu sync.Mutex
|
||
|
||
for _, m := range matches {
|
||
wg.Add(1)
|
||
go func(item matchedHook) {
|
||
defer wg.Done()
|
||
semaphore <- struct{}{}
|
||
defer func() { <-semaphore }()
|
||
|
||
err := item.Client.DeleteHook(item.Owner, item.Repo, item.HookID)
|
||
countMu.Lock()
|
||
defer countMu.Unlock()
|
||
|
||
if err != nil {
|
||
fmt.Printf("❌ Error deleting [%s] %s (Webhook #%d): %v\n", item.Account, item.RepoRef, item.HookID, err)
|
||
} else {
|
||
fmt.Printf("✅ Deleted: [%s] %s -> Webhook #%d (%s)\n", item.Account, item.RepoRef, item.HookID, item.URL)
|
||
successCount++
|
||
}
|
||
}(m)
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
fmt.Printf("\nDone: %d of %d webhook(s) successfully deleted.\n", successCount, len(matches))
|
||
return nil
|
||
},
|
||
}
|
||
|
||
func init() {
|
||
gitCmd.AddCommand(gitWebhookCmd)
|
||
gitWebhookCmd.AddCommand(gitWebhookListCmd)
|
||
gitWebhookCmd.AddCommand(gitWebhookDeleteCmd)
|
||
gitWebhookCmd.AddCommand(gitWebhookPruneCmd)
|
||
|
||
gitWebhookListCmd.ValidArgsFunction = completeRepoNames
|
||
gitWebhookDeleteCmd.ValidArgsFunction = completeRepoNames
|
||
|
||
for _, c := range []*cobra.Command{gitWebhookListCmd, gitWebhookDeleteCmd} {
|
||
c.Flags().StringP("account", "A", "", "Select specific Git account (default: configured default account)")
|
||
_ = c.RegisterFlagCompletionFunc("account", completeGitAccountNames)
|
||
}
|
||
|
||
gitWebhookDeleteCmd.Flags().Int64P("id", "i", 0, "ID of the webhook to delete")
|
||
gitWebhookDeleteCmd.Flags().BoolP("all", "a", false, "Delete all webhooks in the repository")
|
||
gitWebhookDeleteCmd.Flags().StringP("url", "u", "", "Delete webhooks whose target URL contains this string")
|
||
gitWebhookDeleteCmd.Flags().BoolP("yes", "y", false, "Skip interactive confirmation prompt")
|
||
|
||
gitWebhookPruneCmd.Flags().StringP("url", "u", "", "Target URL filter of webhooks to delete (required)")
|
||
gitWebhookPruneCmd.Flags().Bool("dry-run", false, "Preview webhooks that would be deleted without modifying anything")
|
||
gitWebhookPruneCmd.Flags().BoolP("yes", "y", false, "Skip interactive confirmation prompt")
|
||
gitWebhookPruneCmd.Flags().StringP("account", "A", "", "Select specific Git account")
|
||
gitWebhookPruneCmd.Flags().Bool("all-accounts", false, "Search and delete across all configured Git accounts")
|
||
_ = gitWebhookPruneCmd.RegisterFlagCompletionFunc("account", completeGitAccountNames)
|
||
}
|