Skip to content

Commit 812e281

Browse files
author
Binh Nguyen
committed
feat: implement webhook receiver for Docker, GHCR and Harbor to receive update of image pushes and trigger application updates
1 parent 975081e commit 812e281

File tree

13 files changed

+2608
-0
lines changed

13 files changed

+2608
-0
lines changed

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ type ImageUpdaterConfig struct {
5151
GitCommitSignOff bool
5252
DisableKubeEvents bool
5353
GitCreds git.CredsStore
54+
WebhookPort int
55+
EnableWebhook bool
5456
}
5557

5658
// newRootCommand implements the root command of argocd-image-updater
@@ -63,6 +65,7 @@ func newRootCommand() error {
6365
rootCmd.AddCommand(newVersionCommand())
6466
rootCmd.AddCommand(newTestCommand())
6567
rootCmd.AddCommand(newTemplateCommand())
68+
rootCmd.AddCommand(NewWebhookCommand())
6669
err := rootCmd.Execute()
6770
return err
6871
}

cmd/run.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/argoproj-labs/argocd-image-updater/pkg/health"
1616
"github.com/argoproj-labs/argocd-image-updater/pkg/metrics"
1717
"github.com/argoproj-labs/argocd-image-updater/pkg/version"
18+
"github.com/argoproj-labs/argocd-image-updater/pkg/webhook"
1819
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/env"
1920
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
2021
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry"
@@ -144,6 +145,7 @@ func newRunCommand() *cobra.Command {
144145
// Health server will start in a go routine and run asynchronously
145146
var hsErrCh chan error
146147
var msErrCh chan error
148+
var whErrCh chan error
147149
if cfg.HealthPort > 0 {
148150
log.Infof("Starting health probe server TCP port=%d", cfg.HealthPort)
149151
hsErrCh = health.StartHealthServer(cfg.HealthPort)
@@ -179,6 +181,61 @@ func newRunCommand() *cobra.Command {
179181

180182
cfg.GitCreds = cs
181183

184+
// Start the webhook server if enabled
185+
var webhookServer *webhook.WebhookServer
186+
if cfg.EnableWebhook && cfg.WebhookPort > 0 {
187+
// Initialize the ArgoCD client for webhook server
188+
var argoClient argocd.ArgoCD
189+
switch cfg.ApplicationsAPIKind {
190+
case applicationsAPIKindK8S:
191+
argoClient, err = argocd.NewK8SClient(cfg.KubeClient, &argocd.K8SClientOptions{AppNamespace: cfg.AppNamespace})
192+
case applicationsAPIKindArgoCD:
193+
argoClient, err = argocd.NewAPIClient(&cfg.ClientOpts)
194+
}
195+
if err != nil {
196+
log.Fatalf("Could not create ArgoCD client for webhook server: %v", err)
197+
}
198+
199+
// Create webhook handler
200+
handler := webhook.NewWebhookHandler()
201+
202+
// Register supported webhook handlers with default empty secrets
203+
// In production, these would be configured via flags or environment variables
204+
dockerHandler := webhook.NewDockerHubWebhook("")
205+
handler.RegisterHandler(dockerHandler)
206+
207+
ghcrHandler := webhook.NewGHCRWebhook("")
208+
handler.RegisterHandler(ghcrHandler)
209+
210+
harborHandler := webhook.NewHarborWebhook("")
211+
handler.RegisterHandler(harborHandler)
212+
213+
log.Infof("Starting webhook server on port %d", cfg.WebhookPort)
214+
webhookServer = webhook.NewWebhookServer(cfg.WebhookPort, handler, cfg.KubeClient, argoClient)
215+
216+
// Set updater config
217+
updaterConfig := &argocd.UpdaterConfig{
218+
DryRun: cfg.DryRun,
219+
GitCommitUser: cfg.GitCommitUser,
220+
GitCommitEmail: cfg.GitCommitMail,
221+
GitCommitMessage: cfg.GitCommitMessage.Tree.Root.String(),
222+
GitCommitSigningKey: cfg.GitCommitSigningKey,
223+
GitCommitSigningMethod: cfg.GitCommitSigningMethod,
224+
GitCommitSignOff: cfg.GitCommitSignOff,
225+
}
226+
webhookServer.UpdaterConfig = updaterConfig
227+
228+
whErrCh = make(chan error, 1)
229+
go func() {
230+
if err := webhookServer.Start(); err != nil {
231+
log.Errorf("Webhook server error: %v", err)
232+
whErrCh <- err
233+
}
234+
}()
235+
236+
log.Infof("Webhook server started and listening on port %d", cfg.WebhookPort)
237+
}
238+
182239
// This is our main loop. We leave it only when our health probe server
183240
// returns an error.
184241
for {
@@ -189,13 +246,28 @@ func newRunCommand() *cobra.Command {
189246
} else {
190247
log.Infof("Health probe server exited gracefully")
191248
}
249+
// Clean shutdown of webhook server if running
250+
if webhookServer != nil {
251+
if err := webhookServer.Stop(); err != nil {
252+
log.Errorf("Error stopping webhook server: %v", err)
253+
}
254+
}
192255
return nil
193256
case err := <-msErrCh:
194257
if err != nil {
195258
log.Errorf("Metrics server exited with error: %v", err)
196259
} else {
197260
log.Infof("Metrics server exited gracefully")
198261
}
262+
// Clean shutdown of webhook server if running
263+
if webhookServer != nil {
264+
if err := webhookServer.Stop(); err != nil {
265+
log.Errorf("Error stopping webhook server: %v", err)
266+
}
267+
}
268+
return nil
269+
case err := <-whErrCh:
270+
log.Errorf("Webhook server exited with error: %v", err)
199271
return nil
200272
default:
201273
if lastRun.IsZero() || time.Since(lastRun) > cfg.CheckInterval {
@@ -251,6 +323,8 @@ func newRunCommand() *cobra.Command {
251323
runCmd.Flags().BoolVar(&cfg.GitCommitSignOff, "git-commit-sign-off", env.GetBoolVal("GIT_COMMIT_SIGN_OFF", false), "Whether to sign-off git commits")
252324
runCmd.Flags().StringVar(&commitMessagePath, "git-commit-message-path", defaultCommitTemplatePath, "Path to a template to use for Git commit messages")
253325
runCmd.Flags().BoolVar(&cfg.DisableKubeEvents, "disable-kube-events", env.GetBoolVal("IMAGE_UPDATER_KUBE_EVENTS", false), "Disable kubernetes events")
326+
runCmd.Flags().IntVar(&cfg.WebhookPort, "webhook-port", env.ParseNumFromEnv("WEBHOOK_PORT", 8082, 0, 65535), "Port to start the webhook server on, 0 to disable")
327+
runCmd.Flags().BoolVar(&cfg.EnableWebhook, "enable-webhook", env.GetBoolVal("ENABLE_WEBHOOK", false), "Enable webhook server for receiving registry events")
254328

255329
return runCmd
256330
}

cmd/webhook.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"os/signal"
7+
"syscall"
8+
9+
"github.com/argoproj-labs/argocd-image-updater/pkg/argocd"
10+
"github.com/argoproj-labs/argocd-image-updater/pkg/kube"
11+
"github.com/argoproj-labs/argocd-image-updater/pkg/webhook"
12+
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
13+
14+
"github.com/spf13/cobra"
15+
)
16+
17+
// WebhookOptions holds the options for the webhook server
18+
type WebhookOptions struct {
19+
Port int
20+
DockerSecret string
21+
GHCRSecret string
22+
UpdateOnEvent bool
23+
ApplicationsAPIKind string
24+
AppNamespace string
25+
ServerAddr string
26+
Insecure bool
27+
Plaintext bool
28+
GRPCWeb bool
29+
AuthToken string
30+
}
31+
32+
var webhookOpts WebhookOptions
33+
34+
// NewWebhookCommand creates a new webhook command
35+
func NewWebhookCommand() *cobra.Command {
36+
var webhookCmd = &cobra.Command{
37+
Use: "webhook",
38+
Short: "Start webhook server to receive registry events",
39+
Long: `
40+
The webhook command starts a server that listens for webhook events from
41+
container registries. When an event is received, it can trigger an image
42+
update check for the affected images.
43+
44+
Supported registries:
45+
- Docker Hub
46+
- GitHub Container Registry (GHCR)
47+
`,
48+
Run: func(cmd *cobra.Command, args []string) {
49+
runWebhook()
50+
},
51+
}
52+
53+
webhookCmd.Flags().IntVar(&webhookOpts.Port, "port", 8080, "Port to listen on for webhook events")
54+
webhookCmd.Flags().StringVar(&webhookOpts.DockerSecret, "docker-secret", "", "Secret for validating Docker Hub webhooks")
55+
webhookCmd.Flags().StringVar(&webhookOpts.GHCRSecret, "ghcr-secret", "", "Secret for validating GitHub Container Registry webhooks")
56+
webhookCmd.Flags().BoolVar(&webhookOpts.UpdateOnEvent, "update-on-event", true, "Whether to trigger image update checks when webhook events are received")
57+
webhookCmd.Flags().StringVar(&webhookOpts.ApplicationsAPIKind, "applications-api", applicationsAPIKindK8S, "API kind that is used to manage Argo CD applications ('kubernetes' or 'argocd')")
58+
webhookCmd.Flags().StringVar(&webhookOpts.AppNamespace, "application-namespace", "", "namespace where Argo Image Updater will manage applications")
59+
webhookCmd.Flags().StringVar(&webhookOpts.ServerAddr, "argocd-server-addr", "", "address of ArgoCD API server")
60+
webhookCmd.Flags().BoolVar(&webhookOpts.Insecure, "argocd-insecure", false, "(INSECURE) ignore invalid TLS certs for ArgoCD server")
61+
webhookCmd.Flags().BoolVar(&webhookOpts.Plaintext, "argocd-plaintext", false, "(INSECURE) connect without TLS to ArgoCD server")
62+
webhookCmd.Flags().BoolVar(&webhookOpts.GRPCWeb, "argocd-grpc-web", false, "use grpc-web for connection to ArgoCD")
63+
webhookCmd.Flags().StringVar(&webhookOpts.AuthToken, "argocd-auth-token", "", "use token for authenticating to ArgoCD")
64+
65+
return webhookCmd
66+
}
67+
68+
// runWebhook starts the webhook server
69+
func runWebhook() {
70+
log.Infof("Starting webhook server on port %d", webhookOpts.Port)
71+
72+
// Initialize the ArgoCD client
73+
var argoClient argocd.ArgoCD
74+
var err error
75+
76+
// Create Kubernetes client
77+
var kubeClient *kube.ImageUpdaterKubernetesClient
78+
kubeClient, err = getKubeConfig(context.TODO(), "", "")
79+
if err != nil {
80+
log.Fatalf("Could not create Kubernetes client: %v", err)
81+
}
82+
83+
// Set up based on application API kind
84+
if webhookOpts.ApplicationsAPIKind == applicationsAPIKindK8S {
85+
argoClient, err = argocd.NewK8SClient(kubeClient, &argocd.K8SClientOptions{AppNamespace: webhookOpts.AppNamespace})
86+
} else {
87+
// Use defaults if not specified
88+
serverAddr := webhookOpts.ServerAddr
89+
if serverAddr == "" {
90+
serverAddr = defaultArgoCDServerAddr
91+
}
92+
93+
// Check for auth token from environment if not provided
94+
authToken := webhookOpts.AuthToken
95+
if authToken == "" {
96+
if token := os.Getenv("ARGOCD_TOKEN"); token != "" {
97+
authToken = token
98+
}
99+
}
100+
101+
clientOpts := argocd.ClientOptions{
102+
ServerAddr: serverAddr,
103+
Insecure: webhookOpts.Insecure,
104+
Plaintext: webhookOpts.Plaintext,
105+
GRPCWeb: webhookOpts.GRPCWeb,
106+
AuthToken: authToken,
107+
}
108+
argoClient, err = argocd.NewAPIClient(&clientOpts)
109+
}
110+
111+
if err != nil {
112+
log.Fatalf("Could not create ArgoCD client: %v", err)
113+
}
114+
115+
// Create webhook handler
116+
handler := webhook.NewWebhookHandler()
117+
118+
// Register supported webhook handlers
119+
dockerHandler := webhook.NewDockerHubWebhook(webhookOpts.DockerSecret)
120+
handler.RegisterHandler(dockerHandler)
121+
122+
ghcrHandler := webhook.NewGHCRWebhook(webhookOpts.GHCRSecret)
123+
handler.RegisterHandler(ghcrHandler)
124+
125+
// Create webhook server
126+
server := webhook.NewWebhookServer(webhookOpts.Port, handler, kubeClient, argoClient)
127+
128+
// Set up graceful shutdown
129+
stop := make(chan os.Signal, 1)
130+
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
131+
132+
// Start the server in a separate goroutine
133+
go func() {
134+
if err := server.Start(); err != nil {
135+
log.Fatalf("Failed to start webhook server: %v", err)
136+
}
137+
}()
138+
139+
// Wait for interrupt signal
140+
<-stop
141+
142+
// Gracefully shut down the server
143+
log.Infof("Shutting down webhook server")
144+
if err := server.Stop(); err != nil {
145+
log.Errorf("Error stopping webhook server: %v", err)
146+
}
147+
}

pkg/argocd/updater_config.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package argocd
2+
3+
// UpdaterConfig holds configuration for image updating
4+
type UpdaterConfig struct {
5+
// DryRun if true, do not modify anything
6+
DryRun bool
7+
// MaxConcurrency is the maximum number of concurrent update operations
8+
MaxConcurrency int
9+
// GitCommitUser is the user name to use for Git commits
10+
GitCommitUser string
11+
// GitCommitEmail is the email to use for Git commits
12+
GitCommitEmail string
13+
// GitCommitMessage is the template for Git commit messages
14+
GitCommitMessage string
15+
// GitCommitSigningKey is the key to use for signing Git commits
16+
GitCommitSigningKey string
17+
// GitCommitSigningMethod is the method to use for signing Git commits
18+
GitCommitSigningMethod string
19+
// GitCommitSignOff if true, add sign-off line to Git commits
20+
GitCommitSignOff bool
21+
}
22+
23+
// NewUpdaterConfig creates a new UpdaterConfig with default values
24+
func NewUpdaterConfig() *UpdaterConfig {
25+
return &UpdaterConfig{
26+
DryRun: false,
27+
MaxConcurrency: 10,
28+
GitCommitUser: "argocd-image-updater",
29+
GitCommitEmail: "noreply@argoproj.io",
30+
GitCommitMessage: "Update image version",
31+
GitCommitSigningKey: "",
32+
GitCommitSigningMethod: "openpgp",
33+
GitCommitSignOff: false,
34+
}
35+
}

0 commit comments

Comments
 (0)