Files
kmanage/cmd/git_webhook.go

367 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}