From 227a7f85e76a6f62b90bb93fa5b4283654d89075 Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Mon, 5 Feb 2024 12:05:39 +0200 Subject: [PATCH] feat: add support for Rollout kind in Application (#249) --- .../argocd/argo_status_updater.go | 10 +++--- .../argocd/argo_status_updater_test.go | 20 +++++------ cmd/argo-watcher/config/config.go | 35 ++++++++++--------- cmd/argo-watcher/server/server.go | 7 +++- internal/models/argo.go | 8 ++++- internal/models/argo_test.go | 32 ++++++++++++++--- 6 files changed, 74 insertions(+), 38 deletions(-) diff --git a/cmd/argo-watcher/argocd/argo_status_updater.go b/cmd/argo-watcher/argocd/argo_status_updater.go index aa1b9cc7..fb3a478a 100644 --- a/cmd/argo-watcher/argocd/argo_status_updater.go +++ b/cmd/argo-watcher/argocd/argo_status_updater.go @@ -33,9 +33,10 @@ type ArgoStatusUpdater struct { registryProxyUrl string retryOptions []retry.Option mutex MutexMap + acceptSuspended bool } -func (updater *ArgoStatusUpdater) Init(argo Argo, retryAttempts uint, retryDelay time.Duration, registryProxyUrl string) { +func (updater *ArgoStatusUpdater) Init(argo Argo, retryAttempts uint, retryDelay time.Duration, registryProxyUrl string, acceptSuspended bool) { updater.argo = argo updater.registryProxyUrl = registryProxyUrl updater.retryOptions = []retry.Option{ @@ -44,6 +45,7 @@ func (updater *ArgoStatusUpdater) Init(argo Argo, retryAttempts uint, retryDelay retry.Delay(retryDelay), retry.LastErrorOnly(true), } + updater.acceptSuspended = acceptSuspended } func (updater *ArgoStatusUpdater) collectInitialAppStatus(task *models.Task) error { @@ -52,7 +54,7 @@ func (updater *ArgoStatusUpdater) collectInitialAppStatus(task *models.Task) err return err } - status := application.GetRolloutStatus(task.ListImages(), updater.registryProxyUrl) + status := application.GetRolloutStatus(task.ListImages(), updater.registryProxyUrl, updater.acceptSuspended) // sort images to avoid hash mismatch slices.Sort(application.Status.Summary.Images) @@ -79,7 +81,7 @@ func (updater *ArgoStatusUpdater) WaitForRollout(task models.Task) { } // get application status - status := application.GetRolloutStatus(task.ListImages(), updater.registryProxyUrl) + status := application.GetRolloutStatus(task.ListImages(), updater.registryProxyUrl, updater.acceptSuspended) if status == models.ArgoRolloutAppSuccess { log.Info().Str("id", task.Id).Msg("App is running on the expected version.") // deployment success @@ -174,7 +176,7 @@ func (updater *ArgoStatusUpdater) waitForApplicationDeployment(task models.Task) return err } - status := application.GetRolloutStatus(task.ListImages(), updater.registryProxyUrl) + status := application.GetRolloutStatus(task.ListImages(), updater.registryProxyUrl, updater.acceptSuspended) switch status { case models.ArgoRolloutAppDegraded: diff --git a/cmd/argo-watcher/argocd/argo_status_updater_test.go b/cmd/argo-watcher/argocd/argo_status_updater_test.go index 7bf152f4..d0a700d3 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 3, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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, "") + updater.Init(*argo, 1, 0*time.Second, "", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // 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") + updater.Init(*argo, 1, 0*time.Second, "test-registry", false) // prepare test data task := models.Task{ diff --git a/cmd/argo-watcher/config/config.go b/cmd/argo-watcher/config/config.go index b6cd7143..0706598d 100644 --- a/cmd/argo-watcher/config/config.go +++ b/cmd/argo-watcher/config/config.go @@ -28,23 +28,24 @@ type DatabaseConfig struct { } 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. - ArgoToken string `env:"ARGO_TOKEN,required" json:"-"` - ArgoApiTimeout int64 `env:"ARGO_API_TIMEOUT" envDefault:"60" json:"argo_api_timeout"` - DeploymentTimeout uint `env:"DEPLOYMENT_TIMEOUT" envDefault:"900" json:"deployment_timeout"` - ArgoRefreshApp bool `env:"ARGO_REFRESH_APP" envDefault:"true" json:"argo_refresh_app"` - RegistryProxyUrl string `env:"DOCKER_IMAGES_PROXY" json:"registry_proxy_url,omitempty"` - StateType string `env:"STATE_TYPE,required" validate:"oneof=postgres in-memory" json:"state_type"` - StaticFilePath string `env:"STATIC_FILES_PATH" envDefault:"static" json:"-"` - SkipTlsVerify bool `env:"SKIP_TLS_VERIFY" envDefault:"false" json:"skip_tls_verify"` - LogLevel string `env:"LOG_LEVEL" envDefault:"info" json:"log_level"` - LogFormat string `env:"LOG_FORMAT" envDefault:"json" json:"-"` - Host string `env:"HOST" envDefault:"0.0.0.0" json:"-"` - Port string `env:"PORT" envDefault:"8080" json:"-"` - DeployToken string `env:"ARGO_WATCHER_DEPLOY_TOKEN" json:"-"` - Db DatabaseConfig `json:"db,omitempty"` - Keycloak KeycloakConfig `json:"keycloak,omitempty"` + 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. + 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 + DeploymentTimeout uint `env:"DEPLOYMENT_TIMEOUT" envDefault:"900" json:"deployment_timeout"` + ArgoRefreshApp bool `env:"ARGO_REFRESH_APP" envDefault:"true" json:"argo_refresh_app"` + RegistryProxyUrl string `env:"DOCKER_IMAGES_PROXY" json:"registry_proxy_url,omitempty"` + StateType string `env:"STATE_TYPE,required" validate:"oneof=postgres in-memory" json:"state_type"` + StaticFilePath string `env:"STATIC_FILES_PATH" envDefault:"static" json:"-"` + SkipTlsVerify bool `env:"SKIP_TLS_VERIFY" envDefault:"false" json:"skip_tls_verify"` + LogLevel string `env:"LOG_LEVEL" envDefault:"info" json:"log_level"` + LogFormat string `env:"LOG_FORMAT" envDefault:"json" json:"-"` + Host string `env:"HOST" envDefault:"0.0.0.0" json:"-"` + Port string `env:"PORT" envDefault:"8080" json:"-"` + DeployToken string `env:"ARGO_WATCHER_DEPLOY_TOKEN" json:"-"` + Db DatabaseConfig `json:"db,omitempty"` + Keycloak KeycloakConfig `json:"keycloak,omitempty"` } // NewServerConfig parses the server configuration from environment variables using the envconfig package. diff --git a/cmd/argo-watcher/server/server.go b/cmd/argo-watcher/server/server.go index a1e034c6..435b3c59 100644 --- a/cmd/argo-watcher/server/server.go +++ b/cmd/argo-watcher/server/server.go @@ -69,7 +69,12 @@ func RunServer() { // initialize argo updater updater := &argocd.ArgoStatusUpdater{} - updater.Init(*argo, serverConfig.GetRetryAttempts(), argocd.ArgoSyncRetryDelay, serverConfig.RegistryProxyUrl) + updater.Init(*argo, + serverConfig.GetRetryAttempts(), + argocd.ArgoSyncRetryDelay, + serverConfig.RegistryProxyUrl, + serverConfig.AcceptSuspendedApp, + ) // create environment env := &Env{config: serverConfig, argo: argo, metrics: metrics, updater: updater} diff --git a/internal/models/argo.go b/internal/models/argo.go index 33c160c9..f65aa3fa 100644 --- a/internal/models/argo.go +++ b/internal/models/argo.go @@ -78,7 +78,7 @@ type Application struct { } // GetRolloutStatus calculates application rollout status depending on the expected images and proxy configuration. -func (app *Application) GetRolloutStatus(rolloutImages []string, registryProxyUrl string) string { +func (app *Application) GetRolloutStatus(rolloutImages []string, registryProxyUrl string, acceptSuspended bool) string { // check if all the images rolled out for _, image := range rolloutImages { if !helpers.ImagesContains(app.Status.Summary.Images, image, registryProxyUrl) { @@ -96,6 +96,12 @@ func (app *Application) GetRolloutStatus(rolloutImages []string, registryProxyUr return ArgoRolloutAppNotSynced } + // an optional check that helps when we are dealing with Rollout object that can be in a suspended state + // during the rollout process + if app.Status.Health.Status == "Suspended" && app.Status.Sync.Status == "Synced" && acceptSuspended { + return ArgoRolloutAppSuccess + } + // verify app health status if app.Status.Health.Status != "Healthy" { return ArgoRolloutAppNotHealthy diff --git a/internal/models/argo_test.go b/internal/models/argo_test.go index e4057eb1..369bf815 100644 --- a/internal/models/argo_test.go +++ b/internal/models/argo_test.go @@ -71,7 +71,7 @@ func TestArgoRolloutStatus(t *testing.T) { images := []string{"ghcr.io/shini4i/argo-watcher:version2"} registryProxyUrl := "" // test status - assert.Equal(t, ArgoRolloutAppNotAvailable, application.GetRolloutStatus(images, registryProxyUrl)) + assert.Equal(t, ArgoRolloutAppNotAvailable, application.GetRolloutStatus(images, registryProxyUrl, false)) }) t.Run("Rollout status - ArgoRolloutAppNotSynced", func(t *testing.T) { @@ -83,7 +83,7 @@ func TestArgoRolloutStatus(t *testing.T) { images := []string{"ghcr.io/shini4i/argo-watcher:version1"} registryProxyUrl := "" // test status - assert.Equal(t, ArgoRolloutAppNotSynced, application.GetRolloutStatus(images, registryProxyUrl)) + assert.Equal(t, ArgoRolloutAppNotSynced, application.GetRolloutStatus(images, registryProxyUrl, false)) }) t.Run("Rollout status - ArgoRolloutAppNotHealthy", func(t *testing.T) { @@ -96,7 +96,7 @@ func TestArgoRolloutStatus(t *testing.T) { images := []string{"ghcr.io/shini4i/argo-watcher:version1"} registryProxyUrl := "" // test status - assert.Equal(t, ArgoRolloutAppNotHealthy, application.GetRolloutStatus(images, registryProxyUrl)) + assert.Equal(t, ArgoRolloutAppNotHealthy, application.GetRolloutStatus(images, registryProxyUrl, false)) }) t.Run("Rollout status - ArgoRolloutAppSuccess", func(t *testing.T) { @@ -109,7 +109,7 @@ func TestArgoRolloutStatus(t *testing.T) { images := []string{"ghcr.io/shini4i/argo-watcher:version1"} registryProxyUrl := "" // test status - assert.Equal(t, ArgoRolloutAppSuccess, application.GetRolloutStatus(images, registryProxyUrl)) + assert.Equal(t, ArgoRolloutAppSuccess, application.GetRolloutStatus(images, registryProxyUrl, false)) }) t.Run("Rollout status - ArgoRolloutAppDegraded", func(t *testing.T) { @@ -122,7 +122,29 @@ func TestArgoRolloutStatus(t *testing.T) { images := []string{"ghcr.io/shini4i/argo-watcher:version1"} registryProxyUrl := "" // test status - assert.Equal(t, ArgoRolloutAppDegraded, application.GetRolloutStatus(images, registryProxyUrl)) + assert.Equal(t, ArgoRolloutAppDegraded, application.GetRolloutStatus(images, registryProxyUrl, false)) + }) + + t.Run("acceptSuspended is true", func(t *testing.T) { + application := Application{} + application.Status.Health.Status = "Suspended" + application.Status.Sync.Status = "Synced" + + status := application.GetRolloutStatus([]string{}, "", true) + if status != ArgoRolloutAppSuccess { + t.Errorf("Expected status to be %s, but got %s", ArgoRolloutAppSuccess, status) + } + }) + + t.Run("acceptSuspended is false", func(t *testing.T) { + application := Application{} + application.Status.Health.Status = "Suspended" + application.Status.Sync.Status = "Synced" + + status := application.GetRolloutStatus([]string{}, "", false) + if status == ArgoRolloutAppSuccess { + t.Errorf("Expected status to not be %s", ArgoRolloutAppSuccess) + } }) }