From d8f2237aa528b4f783b07983c0a553ea03a5ac04 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 01:27:22 +0200 Subject: [PATCH 01/11] feat: webhook skeleton --- cmd/argo-watcher/config/config.go | 6 +++ cmd/argo-watcher/notifications/webhook.go | 48 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 cmd/argo-watcher/notifications/webhook.go diff --git a/cmd/argo-watcher/config/config.go b/cmd/argo-watcher/config/config.go index d9fd0234..745b411f 100644 --- a/cmd/argo-watcher/config/config.go +++ b/cmd/argo-watcher/config/config.go @@ -30,6 +30,11 @@ type DatabaseConfig struct { DSN string `env:"DB_DSN,expand" envDefault:"host=${DB_HOST} port=${DB_PORT} user=${DB_USER} password=${DB_PASSWORD} dbname=${DB_NAME} sslmode=${DB_SSL_MODE} TimeZone=${DB_TIMEZONE}"` } +type WebhookConfig struct { + URL string `env:"WEBHOOK_URL" json:"url,omitempty"` + Format string `env:"WEBHOOK_FORMAT" json:"format,omitempty"` +} + type ServerConfig struct { ArgoUrl url.URL `env:"ARGO_URL,required" json:"argo_cd_url"` ArgoUrlAlias string `env:"ARGO_URL_ALIAS" json:"argo_cd_url_alias,omitempty"` // Used to generate App URL. Can be omitted if ArgoUrl is reachable from outside. @@ -51,6 +56,7 @@ type ServerConfig struct { Keycloak KeycloakConfig `json:"keycloak,omitempty"` ScheduledLockdownEnabled bool `env:"SCHEDULED_LOCKDOWN_ENABLED" envDefault:"false" json:"scheduled_lockdown_enabled"` LockdownSchedule LockdownSchedules `env:"LOCKDOWN_SCHEDULE" json:"-"` + Webhook WebhookConfig `json:"webhook,omitempty"` } // NewServerConfig parses the server configuration from environment variables using the envconfig package. diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go new file mode 100644 index 00000000..dd381353 --- /dev/null +++ b/cmd/argo-watcher/notifications/webhook.go @@ -0,0 +1,48 @@ +package notifications + +import ( + "bytes" + "io" + "net/http" + "text/template" + + "github.com/rs/zerolog/log" + + "github.com/shini4i/argo-watcher/cmd/argo-watcher/config" + "github.com/shini4i/argo-watcher/internal/models" +) + +type WebhookService struct { + serverConfig *config.ServerConfig +} + +func NewWebhookService(serverConfig *config.ServerConfig) *WebhookService { + return &WebhookService{ + serverConfig: serverConfig, + } +} + +func (service *WebhookService) SendWebhook(task models.Task) error { + tmpl, err := template.New("webhook").Parse(service.serverConfig.Webhook.Format) + if err != nil { + return err + } + + var payload bytes.Buffer + if err := tmpl.Execute(&payload, task); err != nil { + return err + } + + resp, err := http.Post(service.serverConfig.Webhook.URL, "application/json", &payload) + if err != nil { + return err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Error().Err(err).Msg("Failed to close response body") + } + }(resp.Body) + + return nil +} From 122a1d2ec156de0561b8876429976cfe3a184a92 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 13:43:24 +0200 Subject: [PATCH 02/11] add a simple test --- cmd/argo-watcher/config/config.go | 4 +- cmd/argo-watcher/notifications/webhook.go | 2 +- .../notifications/webhook_test.go | 63 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 cmd/argo-watcher/notifications/webhook_test.go diff --git a/cmd/argo-watcher/config/config.go b/cmd/argo-watcher/config/config.go index 745b411f..9190dfb8 100644 --- a/cmd/argo-watcher/config/config.go +++ b/cmd/argo-watcher/config/config.go @@ -31,13 +31,13 @@ type DatabaseConfig struct { } type WebhookConfig struct { - URL string `env:"WEBHOOK_URL" json:"url,omitempty"` + Url string `env:"WEBHOOK_URL" json:"url,omitempty"` Format string `env:"WEBHOOK_FORMAT" json:"format,omitempty"` } type ServerConfig struct { ArgoUrl url.URL `env:"ARGO_URL,required" json:"argo_cd_url"` - ArgoUrlAlias string `env:"ARGO_URL_ALIAS" json:"argo_cd_url_alias,omitempty"` // Used to generate App URL. Can be omitted if ArgoUrl is reachable from outside. + ArgoUrlAlias string `env:"ARGO_URL_ALIAS" json:"argo_cd_url_alias,omitempty"` // Used to generate App Url. Can be omitted if ArgoUrl is reachable from outside. ArgoToken string `env:"ARGO_TOKEN,required" json:"-"` ArgoApiTimeout int64 `env:"ARGO_API_TIMEOUT" envDefault:"60" json:"argo_api_timeout"` AcceptSuspendedApp bool `env:"ACCEPT_SUSPENDED_APP" envDefault:"false" json:"accept_suspended_app"` // If true, we will accept "Suspended" health status as valid diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index dd381353..d5dbb47c 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -33,7 +33,7 @@ func (service *WebhookService) SendWebhook(task models.Task) error { return err } - resp, err := http.Post(service.serverConfig.Webhook.URL, "application/json", &payload) + resp, err := http.Post(service.serverConfig.Webhook.Url, "application/json", &payload) if err != nil { return err } diff --git a/cmd/argo-watcher/notifications/webhook_test.go b/cmd/argo-watcher/notifications/webhook_test.go new file mode 100644 index 00000000..d8ee97a9 --- /dev/null +++ b/cmd/argo-watcher/notifications/webhook_test.go @@ -0,0 +1,63 @@ +package notifications + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/shini4i/argo-watcher/cmd/argo-watcher/config" + "github.com/shini4i/argo-watcher/internal/models" + "github.com/stretchr/testify/assert" +) + +type TestWebhookPayload struct { + Id string `json:"id"` + App string `json:"app"` + Status string `json:"status"` +} + +var mockTask = models.Task{ + Id: "test-id", + App: "test-app", + Status: "test-status", +} + +func TestSendWebhook(t *testing.T) { + // Create a test server that checks the received payload + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + t.Errorf("Failed to close response body: %v", err) + } + }(r.Body) + + var payload TestWebhookPayload + err := json.Unmarshal(body, &payload) + assert.NoError(t, err) + assert.Equal(t, mockTask.Id, payload.Id) + assert.Equal(t, mockTask.App, payload.App) + assert.Equal(t, mockTask.Status, payload.Status) + })) + defer testServer.Close() + + // Create a mock ServerConfig with the test server's URL + serverConfig := &config.ServerConfig{ + Webhook: config.WebhookConfig{ + Url: testServer.URL, + Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, + }, + } + + // Create a new WebhookService with the mock ServerConfig + service := NewWebhookService(serverConfig) + + // Call the SendWebhook method with the mock Task + err := service.SendWebhook(mockTask) + + // Check the returned error + assert.NoError(t, err) +} From b78507dbf14b4b116f7190ea445f9e75ce25e502 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 14:01:06 +0200 Subject: [PATCH 03/11] extend test --- cmd/argo-watcher/notifications/webhook.go | 8 +++ .../notifications/webhook_test.go | 67 +++++++++++++------ 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index d5dbb47c..90803837 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -2,6 +2,7 @@ package notifications import ( "bytes" + "fmt" "io" "net/http" "text/template" @@ -33,10 +34,13 @@ func (service *WebhookService) SendWebhook(task models.Task) error { return err } + log.Debug().Str("id", task.Id).Msgf("Sending webhook payload: %s", payload.String()) + resp, err := http.Post(service.serverConfig.Webhook.Url, "application/json", &payload) if err != nil { return err } + defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { @@ -44,5 +48,9 @@ func (service *WebhookService) SendWebhook(task models.Task) error { } }(resp.Body) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("received non-OK status code: %v", resp.StatusCode) + } + return nil } diff --git a/cmd/argo-watcher/notifications/webhook_test.go b/cmd/argo-watcher/notifications/webhook_test.go index d8ee97a9..24a0c9ef 100644 --- a/cmd/argo-watcher/notifications/webhook_test.go +++ b/cmd/argo-watcher/notifications/webhook_test.go @@ -24,40 +24,65 @@ var mockTask = models.Task{ Status: "test-status", } -func TestSendWebhook(t *testing.T) { - // Create a test server that checks the received payload - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func setupTestServer(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - t.Errorf("Failed to close response body: %v", err) + t.Error(err) } }(r.Body) var payload TestWebhookPayload err := json.Unmarshal(body, &payload) assert.NoError(t, err) - assert.Equal(t, mockTask.Id, payload.Id) - assert.Equal(t, mockTask.App, payload.App) - assert.Equal(t, mockTask.Status, payload.Status) + checkPayload(t, payload) + })) +} + +func setupErrorTestServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) })) - defer testServer.Close() +} - // Create a mock ServerConfig with the test server's URL - serverConfig := &config.ServerConfig{ - Webhook: config.WebhookConfig{ - Url: testServer.URL, - Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, - }, - } +func checkPayload(t *testing.T, payload TestWebhookPayload) { + assert.Equal(t, mockTask.Id, payload.Id) + assert.Equal(t, mockTask.App, payload.App) + assert.Equal(t, mockTask.Status, payload.Status) +} + +func TestSendWebhook(t *testing.T) { + t.Run("Test webhook payload", func(t *testing.T) { + testServer := setupTestServer(t) + defer testServer.Close() + + serverConfig := &config.ServerConfig{ + Webhook: config.WebhookConfig{ + Url: testServer.URL, + Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, + }, + } + + service := NewWebhookService(serverConfig) + err := service.SendWebhook(mockTask) + assert.NoError(t, err) + }) - // Create a new WebhookService with the mock ServerConfig - service := NewWebhookService(serverConfig) + t.Run("Test error response", func(t *testing.T) { + testServer := setupErrorTestServer() + defer testServer.Close() - // Call the SendWebhook method with the mock Task - err := service.SendWebhook(mockTask) + serverConfig := &config.ServerConfig{ + Webhook: config.WebhookConfig{ + Url: testServer.URL, + Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, + }, + } - // Check the returned error - assert.NoError(t, err) + service := NewWebhookService(serverConfig) + err := service.SendWebhook(mockTask) + assert.Error(t, err) + }) } From d346743a27cb77b9833af6b0bb20709a148cafdb Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 14:25:54 +0200 Subject: [PATCH 04/11] add a simple poc --- .../argocd/argo_status_updater.go | 21 ++++++++++++++++++- cmd/argo-watcher/config/config.go | 5 +++-- cmd/argo-watcher/notifications/webhook.go | 12 ++++++----- .../notifications/webhook_test.go | 20 +++++++----------- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/cmd/argo-watcher/argocd/argo_status_updater.go b/cmd/argo-watcher/argocd/argo_status_updater.go index 21e05b03..7ec3f632 100644 --- a/cmd/argo-watcher/argocd/argo_status_updater.go +++ b/cmd/argo-watcher/argocd/argo_status_updater.go @@ -9,6 +9,9 @@ import ( "sync" "time" + "github.com/shini4i/argo-watcher/cmd/argo-watcher/config" + "github.com/shini4i/argo-watcher/cmd/argo-watcher/notifications" + "github.com/shini4i/argo-watcher/internal/helpers" "github.com/avast/retry-go/v4" @@ -34,9 +37,10 @@ type ArgoStatusUpdater struct { retryOptions []retry.Option mutex MutexMap acceptSuspended bool + webhookService *notifications.WebhookService } -func (updater *ArgoStatusUpdater) Init(argo Argo, retryAttempts uint, retryDelay time.Duration, registryProxyUrl string, acceptSuspended bool) { +func (updater *ArgoStatusUpdater) Init(argo Argo, retryAttempts uint, retryDelay time.Duration, registryProxyUrl string, acceptSuspended bool, webhookConfig *config.WebhookConfig) { updater.argo = argo updater.registryProxyUrl = registryProxyUrl updater.retryOptions = []retry.Option{ @@ -46,6 +50,7 @@ func (updater *ArgoStatusUpdater) Init(argo Argo, retryAttempts uint, retryDelay retry.LastErrorOnly(true), } updater.acceptSuspended = acceptSuspended + updater.webhookService = notifications.NewWebhookService(webhookConfig) } func (updater *ArgoStatusUpdater) collectInitialAppStatus(task *models.Task) error { @@ -68,6 +73,9 @@ func (updater *ArgoStatusUpdater) collectInitialAppStatus(task *models.Task) err } func (updater *ArgoStatusUpdater) WaitForRollout(task models.Task) { + // notify about the deployment start + sendWebhookEvent(task, updater.webhookService) + // wait for application to get into deployed status or timeout application, err := updater.waitForApplicationDeployment(task) @@ -107,6 +115,9 @@ func (updater *ArgoStatusUpdater) WaitForRollout(task models.Task) { log.Error().Str("id", task.Id).Msgf(failedToUpdateTaskStatusTemplate, errStatusChange) } } + + // send webhook event about the deployment result + sendWebhookEvent(task, updater.webhookService) } func (updater *ArgoStatusUpdater) waitForApplicationDeployment(task models.Task) (*models.Application, error) { @@ -238,3 +249,11 @@ func (updater *ArgoStatusUpdater) handleArgoAPIFailure(task models.Task, err err log.Error().Str("id", task.Id).Msgf(failedToUpdateTaskStatusTemplate, err) } } + +func sendWebhookEvent(task models.Task, webhookService *notifications.WebhookService) { + if webhookService.Enabled { + if err := webhookService.SendWebhook(task); err != nil { + log.Error().Str("id", task.Id).Msgf("Failed to send webhook. Error: %s", err.Error()) + } + } +} diff --git a/cmd/argo-watcher/config/config.go b/cmd/argo-watcher/config/config.go index 9190dfb8..e6716f07 100644 --- a/cmd/argo-watcher/config/config.go +++ b/cmd/argo-watcher/config/config.go @@ -31,8 +31,9 @@ type DatabaseConfig struct { } type WebhookConfig struct { - Url string `env:"WEBHOOK_URL" json:"url,omitempty"` - Format string `env:"WEBHOOK_FORMAT" json:"format,omitempty"` + Enabled bool `env:"WEBHOOK_ENABLED" envDefault:"false" json:"enabled"` + Url string `env:"WEBHOOK_URL" json:"url,omitempty"` + Format string `env:"WEBHOOK_FORMAT" json:"format,omitempty"` } type ServerConfig struct { diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index 90803837..de404e43 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -14,17 +14,19 @@ import ( ) type WebhookService struct { - serverConfig *config.ServerConfig + Enabled bool + config *config.WebhookConfig } -func NewWebhookService(serverConfig *config.ServerConfig) *WebhookService { +func NewWebhookService(webhookConfig *config.WebhookConfig) *WebhookService { return &WebhookService{ - serverConfig: serverConfig, + Enabled: webhookConfig.Enabled, + config: webhookConfig, } } func (service *WebhookService) SendWebhook(task models.Task) error { - tmpl, err := template.New("webhook").Parse(service.serverConfig.Webhook.Format) + tmpl, err := template.New("webhook").Parse(service.config.Format) if err != nil { return err } @@ -36,7 +38,7 @@ func (service *WebhookService) SendWebhook(task models.Task) error { log.Debug().Str("id", task.Id).Msgf("Sending webhook payload: %s", payload.String()) - resp, err := http.Post(service.serverConfig.Webhook.Url, "application/json", &payload) + resp, err := http.Post(service.config.Url, "application/json", &payload) if err != nil { return err } diff --git a/cmd/argo-watcher/notifications/webhook_test.go b/cmd/argo-watcher/notifications/webhook_test.go index 24a0c9ef..a12b99a3 100644 --- a/cmd/argo-watcher/notifications/webhook_test.go +++ b/cmd/argo-watcher/notifications/webhook_test.go @@ -58,14 +58,12 @@ func TestSendWebhook(t *testing.T) { testServer := setupTestServer(t) defer testServer.Close() - serverConfig := &config.ServerConfig{ - Webhook: config.WebhookConfig{ - Url: testServer.URL, - Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, - }, + webhookConfig := config.WebhookConfig{ + Url: testServer.URL, + Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, } - service := NewWebhookService(serverConfig) + service := NewWebhookService(&webhookConfig) err := service.SendWebhook(mockTask) assert.NoError(t, err) }) @@ -74,14 +72,12 @@ func TestSendWebhook(t *testing.T) { testServer := setupErrorTestServer() defer testServer.Close() - serverConfig := &config.ServerConfig{ - Webhook: config.WebhookConfig{ - Url: testServer.URL, - Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, - }, + webhookConfig := config.WebhookConfig{ + Url: testServer.URL, + Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, } - service := NewWebhookService(serverConfig) + service := NewWebhookService(&webhookConfig) err := service.SendWebhook(mockTask) assert.Error(t, err) }) From 104a243d1a38ae77c815d9364365a814c7eb9c1e Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 14:34:33 +0200 Subject: [PATCH 05/11] fix --- .../argocd/argo_status_updater_test.go | 20 +++++++++---------- cmd/argo-watcher/server/server.go | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/argo-watcher/argocd/argo_status_updater_test.go b/cmd/argo-watcher/argocd/argo_status_updater_test.go index d0a700d3..be546993 100644 --- a/cmd/argo-watcher/argocd/argo_status_updater_test.go +++ b/cmd/argo-watcher/argocd/argo_status_updater_test.go @@ -29,7 +29,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -70,7 +70,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 3, 0*time.Second, "test-registry", false) + updater.Init(*argo, 3, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -118,7 +118,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -159,7 +159,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "", false) + updater.Init(*argo, 1, 0*time.Second, "", false, nil) // prepare test data task := models.Task{ @@ -201,7 +201,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -230,7 +230,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -259,7 +259,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -288,7 +288,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -328,7 +328,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ @@ -372,7 +372,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) // prepare test data task := models.Task{ diff --git a/cmd/argo-watcher/server/server.go b/cmd/argo-watcher/server/server.go index 1e54a846..ece111dc 100644 --- a/cmd/argo-watcher/server/server.go +++ b/cmd/argo-watcher/server/server.go @@ -74,6 +74,7 @@ func RunServer() { argocd.ArgoSyncRetryDelay, serverConfig.RegistryProxyUrl, serverConfig.AcceptSuspendedApp, + &serverConfig.Webhook, ) // create environment From c357e85b02d60208e05266427dacf62f1c1048d3 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 14:43:19 +0200 Subject: [PATCH 06/11] fix test --- cmd/argo-watcher/notifications/webhook.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index de404e43..78df6b09 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -19,8 +19,13 @@ type WebhookService struct { } func NewWebhookService(webhookConfig *config.WebhookConfig) *WebhookService { + if webhookConfig == nil { + return &WebhookService{ + Enabled: false, + } + } return &WebhookService{ - Enabled: webhookConfig.Enabled, + Enabled: true, config: webhookConfig, } } From f7bbd2243390b2cfe88b8f32f285778f168d0200 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 14:48:10 +0200 Subject: [PATCH 07/11] chore: more reasonable approach --- .../argocd/argo_status_updater_test.go | 28 ++++++++++++------- cmd/argo-watcher/notifications/webhook.go | 5 ---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/cmd/argo-watcher/argocd/argo_status_updater_test.go b/cmd/argo-watcher/argocd/argo_status_updater_test.go index be546993..fb0804ff 100644 --- a/cmd/argo-watcher/argocd/argo_status_updater_test.go +++ b/cmd/argo-watcher/argocd/argo_status_updater_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/shini4i/argo-watcher/cmd/argo-watcher/config" + "github.com/stretchr/testify/assert" "github.com/shini4i/argo-watcher/cmd/argo-watcher/mock" @@ -13,6 +15,12 @@ import ( "go.uber.org/mock/gomock" ) +var ( + mockWebhookConfig = &config.WebhookConfig{ + Enabled: false, + } +) + func TestArgoStatusUpdaterCheck(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -29,7 +37,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -70,7 +78,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 3, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 3, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -118,7 +126,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -159,7 +167,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "", false, nil) + updater.Init(*argo, 1, 0*time.Second, "", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -201,7 +209,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -230,7 +238,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -259,7 +267,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -288,7 +296,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -328,7 +336,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ @@ -372,7 +380,7 @@ func TestArgoStatusUpdaterCheck(t *testing.T) { // argo updater updater := &ArgoStatusUpdater{} - updater.Init(*argo, 1, 0*time.Second, "test-registry", false, nil) + updater.Init(*argo, 1, 0*time.Second, "test-registry", false, mockWebhookConfig) // prepare test data task := models.Task{ diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index 78df6b09..ab35e90c 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -19,11 +19,6 @@ type WebhookService struct { } func NewWebhookService(webhookConfig *config.WebhookConfig) *WebhookService { - if webhookConfig == nil { - return &WebhookService{ - Enabled: false, - } - } return &WebhookService{ Enabled: true, config: webhookConfig, From d256a9ac944785c5b9ee5102a094743da2300536 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sat, 30 Mar 2024 16:01:40 +0200 Subject: [PATCH 08/11] remove hardcoded value --- cmd/argo-watcher/notifications/webhook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index ab35e90c..de404e43 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -20,7 +20,7 @@ type WebhookService struct { func NewWebhookService(webhookConfig *config.WebhookConfig) *WebhookService { return &WebhookService{ - Enabled: true, + Enabled: webhookConfig.Enabled, config: webhookConfig, } } From 60902be9cb6b82e72545d74659ea6369e246b402 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 31 Mar 2024 01:21:20 +0200 Subject: [PATCH 09/11] fix notification logic --- cmd/argo-watcher/argocd/argo_status_updater.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cmd/argo-watcher/argocd/argo_status_updater.go b/cmd/argo-watcher/argocd/argo_status_updater.go index 7ec3f632..8c1fa652 100644 --- a/cmd/argo-watcher/argocd/argo_status_updater.go +++ b/cmd/argo-watcher/argocd/argo_status_updater.go @@ -95,10 +95,11 @@ func (updater *ArgoStatusUpdater) WaitForRollout(task models.Task) { // deployment success updater.argo.metrics.ResetFailedDeployment(task.App) // update task status - errStatusChange := updater.argo.State.SetTaskStatus(task.Id, models.StatusDeployedMessage, "") - if errStatusChange != nil { - log.Error().Str("id", task.Id).Msgf(failedToUpdateTaskStatusTemplate, errStatusChange) + if err := updater.argo.State.SetTaskStatus(task.Id, models.StatusDeployedMessage, ""); err != nil { + log.Error().Str("id", task.Id).Msgf(failedToUpdateTaskStatusTemplate, err) } + // setting task status to handle further notifications + task.Status = models.StatusDeployedMessage } else { log.Info().Str("id", task.Id).Msg("App deployment failed.") // deployment failed @@ -110,10 +111,11 @@ func (updater *ArgoStatusUpdater) WaitForRollout(task models.Task) { application.GetRolloutMessage(status, task.ListImages()), ) // update task status - errStatusChange := updater.argo.State.SetTaskStatus(task.Id, models.StatusFailedMessage, reason) - if errStatusChange != nil { - log.Error().Str("id", task.Id).Msgf(failedToUpdateTaskStatusTemplate, errStatusChange) + if err := updater.argo.State.SetTaskStatus(task.Id, models.StatusFailedMessage, reason); err != nil { + log.Error().Str("id", task.Id).Msgf(failedToUpdateTaskStatusTemplate, err) } + // setting task status to handle further notifications + task.Status = models.StatusFailedMessage } // send webhook event about the deployment result From 0d5e39021d1807ee4ee7a6410ba4bb10e82d0a54 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 31 Mar 2024 18:02:39 +0300 Subject: [PATCH 10/11] chore: add support for providing authorization header --- cmd/argo-watcher/config/config.go | 8 +++++--- cmd/argo-watcher/notifications/webhook.go | 11 ++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/argo-watcher/config/config.go b/cmd/argo-watcher/config/config.go index e6716f07..d408eec0 100644 --- a/cmd/argo-watcher/config/config.go +++ b/cmd/argo-watcher/config/config.go @@ -31,9 +31,11 @@ type DatabaseConfig struct { } type WebhookConfig struct { - Enabled bool `env:"WEBHOOK_ENABLED" envDefault:"false" json:"enabled"` - Url string `env:"WEBHOOK_URL" json:"url,omitempty"` - Format string `env:"WEBHOOK_FORMAT" json:"format,omitempty"` + Enabled bool `env:"WEBHOOK_ENABLED" envDefault:"false" json:"enabled"` + Url string `env:"WEBHOOK_URL" json:"url,omitempty"` + Format string `env:"WEBHOOK_FORMAT" json:"format,omitempty"` + AuthorizationHeader string `env:"WEBHOOK_AUTHORIZATION_HEADER_NAME" envDefault:"Authorization" json:"authorization_header,omitempty"` + Token string `env:"WEBHOOK_AUTHORIZATION_HEADER_VALUE" envDefault:"" json:"-"` } type ServerConfig struct { diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index de404e43..09879f90 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -16,12 +16,14 @@ import ( type WebhookService struct { Enabled bool config *config.WebhookConfig + client *http.Client } func NewWebhookService(webhookConfig *config.WebhookConfig) *WebhookService { return &WebhookService{ Enabled: webhookConfig.Enabled, config: webhookConfig, + client: &http.Client{}, } } @@ -38,11 +40,18 @@ func (service *WebhookService) SendWebhook(task models.Task) error { log.Debug().Str("id", task.Id).Msgf("Sending webhook payload: %s", payload.String()) - resp, err := http.Post(service.config.Url, "application/json", &payload) + req, err := http.NewRequest("POST", service.config.Url, &payload) if err != nil { return err } + req.Header.Set("Content-Type", "application/json") + if service.config.Token != "" { + req.Header.Set(service.config.AuthorizationHeader, service.config.Token) + } + + resp, err := service.client.Do(req) + defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { From 05b0bc4b394d96779f9442f89a7e764e74470e25 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 31 Mar 2024 18:16:54 +0300 Subject: [PATCH 11/11] fix test --- cmd/argo-watcher/notifications/webhook.go | 3 +++ cmd/argo-watcher/notifications/webhook_test.go | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cmd/argo-watcher/notifications/webhook.go b/cmd/argo-watcher/notifications/webhook.go index 09879f90..3b599c18 100644 --- a/cmd/argo-watcher/notifications/webhook.go +++ b/cmd/argo-watcher/notifications/webhook.go @@ -51,6 +51,9 @@ func (service *WebhookService) SendWebhook(task models.Task) error { } resp, err := service.client.Do(req) + if err != nil { + return err + } defer func(Body io.ReadCloser) { err := Body.Close() diff --git a/cmd/argo-watcher/notifications/webhook_test.go b/cmd/argo-watcher/notifications/webhook_test.go index a12b99a3..d5f62a27 100644 --- a/cmd/argo-watcher/notifications/webhook_test.go +++ b/cmd/argo-watcher/notifications/webhook_test.go @@ -24,8 +24,13 @@ var mockTask = models.Task{ Status: "test-status", } +const expectedAuthToken = "Bearer the-test-token" + func setupTestServer(t *testing.T) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + assert.Equal(t, expectedAuthToken, token) + body, _ := io.ReadAll(r.Body) defer func(Body io.ReadCloser) { err := Body.Close() @@ -59,8 +64,10 @@ func TestSendWebhook(t *testing.T) { defer testServer.Close() webhookConfig := config.WebhookConfig{ - Url: testServer.URL, - Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, + Url: testServer.URL, + Format: `{"id": "{{.Id}}","app": "{{.App}}","status": "{{.Status}}"}`, + AuthorizationHeader: "Authorization", + Token: expectedAuthToken, } service := NewWebhookService(&webhookConfig)