From c0205fb7309b870f9e3313735699beffd1e74eae Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 26 May 2024 00:21:30 +0300 Subject: [PATCH 1/2] chore(client): add support for additional configuration --- pkg/client/client.go | 17 ++++++++++++++--- pkg/client/client_test.go | 24 ++++++++++++++++++++++++ pkg/client/config.go | 30 ++++++++++++++++-------------- pkg/client/utility.go | 4 ++-- pkg/client/utility_test.go | 8 ++++---- 5 files changed, 60 insertions(+), 23 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index b0e481e..315597c 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -21,7 +21,7 @@ import ( ) var ( - clientConfig *ClientConfig + clientConfig *Config ) type Watcher struct { @@ -124,6 +124,8 @@ func (watcher *Watcher) getWatcherConfig() (*config.ServerConfig, error) { } func (watcher *Watcher) waitForDeployment(id, appName, version string) error { + retryCount := 0 + for { taskInfo, err := watcher.getTaskStatus(id) if err != nil { @@ -134,8 +136,13 @@ func (watcher *Watcher) waitForDeployment(id, appName, version string) error { case models.StatusFailedMessage: return fmt.Errorf("The deployment has failed, please check logs.\n%s", taskInfo.StatusReason) case models.StatusInProgressMessage: - log.Println("Application deployment is in progress...") - time.Sleep(15 * time.Second) + if !isDeploymentOverTime(retryCount, clientConfig.RetryInterval, clientConfig.ExpectedDeploymentTime) { + log.Println("Application deployment is in progress...") + } else { + log.Println("Application deployment is taking longer than expected, it might be worth checking ArgoCD UI...") + } + retryCount++ + time.Sleep(clientConfig.RetryInterval) case models.StatusAppNotFoundMessage: return fmt.Errorf("Application %s does not exist.\n%s", appName, taskInfo.StatusReason) case models.StatusArgoCDUnavailableMessage: @@ -163,6 +170,10 @@ func handleFatalError(err error, message string) { log.Fatalf("%s Got the following error: %s", message, err) } +func isDeploymentOverTime(retryCount int, retryInterval time.Duration, expectedDeploymentTime time.Duration) bool { + return time.Duration(retryCount)*retryInterval > expectedDeploymentTime +} + func Run() { var err error diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 2700807..4da4de4 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -280,3 +280,27 @@ func TestWaitForDeployment(t *testing.T) { }) } } + +func TestIsDeploymentOverTime(t *testing.T) { + var tests = []struct { + retryCount int + retryInterval time.Duration + expectedDuration time.Duration + expected bool + }{ + {10, 5 * time.Second, 1 * time.Minute, false}, + {13, 5 * time.Second, 1 * time.Minute, true}, + {7, 10 * time.Second, 1 * time.Minute, true}, + {7, 15 * time.Second, 1 * time.Minute, true}, + {0, 2 * time.Second, 1 * time.Minute, false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := isDeploymentOverTime(tt.retryCount, tt.retryInterval, tt.expectedDuration) + if result != tt.expected { + t.Errorf("for %d retries with %s interval, expected %t but got %t", tt.retryCount, tt.retryInterval, tt.expected, result) + } + }) + } +} diff --git a/pkg/client/config.go b/pkg/client/config.go index cf05f39..11e4e64 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -6,22 +6,24 @@ import ( envConfig "github.com/caarlos0/env/v10" ) -type ClientConfig struct { - Url string `env:"ARGO_WATCHER_URL,required"` - Images []string `env:"IMAGES,required"` - Tag string `env:"IMAGE_TAG,required"` - App string `env:"ARGO_APP,required"` - Author string `env:"COMMIT_AUTHOR,required"` - Project string `env:"PROJECT_NAME,required"` - Token string `env:"ARGO_WATCHER_DEPLOY_TOKEN"` - JsonWebToken string `env:"BEARER_TOKEN"` - Timeout time.Duration `env:"TIMEOUT" envDefault:"60s"` - TaskTimeout int `env:"TASK_TIMEOUT"` - Debug bool `env:"DEBUG"` +type Config struct { + Url string `env:"ARGO_WATCHER_URL,required"` + Images []string `env:"IMAGES,required"` + Tag string `env:"IMAGE_TAG,required"` + App string `env:"ARGO_APP,required"` + Author string `env:"COMMIT_AUTHOR,required"` + Project string `env:"PROJECT_NAME,required"` + Token string `env:"ARGO_WATCHER_DEPLOY_TOKEN"` + JsonWebToken string `env:"BEARER_TOKEN"` + Timeout time.Duration `env:"TIMEOUT" envDefault:"60s"` + TaskTimeout int `env:"TASK_TIMEOUT"` + RetryInterval time.Duration `env:"RETRY_INTERVAL" envDefault:"15s"` + ExpectedDeploymentTime time.Duration `env:"EXPECTED_DEPLOY_TIME" envDefault:"15m"` + Debug bool `env:"DEBUG"` } -func NewClientConfig() (*ClientConfig, error) { - var config ClientConfig +func NewClientConfig() (*Config, error) { + var config Config if err := envConfig.Parse(&config); err != nil { return nil, err diff --git a/pkg/client/utility.go b/pkg/client/utility.go index b5dbb04..afc4a5f 100644 --- a/pkg/client/utility.go +++ b/pkg/client/utility.go @@ -49,7 +49,7 @@ func getImagesList(list []string, tag string) []models.Image { return images } -func createTask(config *ClientConfig) models.Task { +func createTask(config *Config) models.Task { images := getImagesList(config.Images, config.Tag) return models.Task{ App: config.App, @@ -86,7 +86,7 @@ func generateAppUrl(watcher *Watcher, task models.Task) (string, error) { return fmt.Sprintf("%s://%s/applications/%s", cfg.ArgoUrl.Scheme, cfg.ArgoUrl.Host, task.App), nil } -func setupWatcher(config *ClientConfig) *Watcher { +func setupWatcher(config *Config) *Watcher { return NewWatcher( strings.TrimSuffix(config.Url, "/"), config.Debug, diff --git a/pkg/client/utility_test.go b/pkg/client/utility_test.go index 76e52d5..077c72a 100644 --- a/pkg/client/utility_test.go +++ b/pkg/client/utility_test.go @@ -96,7 +96,7 @@ func TestGetImagesList(t *testing.T) { func TestCreateTask(t *testing.T) { t.Run("TimeoutProvided", func(t *testing.T) { - config := &ClientConfig{ + config := &Config{ App: "test-app", Author: "test-author", Project: "test-project", @@ -128,7 +128,7 @@ func TestCreateTask(t *testing.T) { }) t.Run("TimeoutNotProvided", func(t *testing.T) { - config := &ClientConfig{ + config := &Config{ App: "test-app", Author: "test-author", Project: "test-project", @@ -161,7 +161,7 @@ func TestCreateTask(t *testing.T) { func TestPrintClientConfiguration(t *testing.T) { // Initialize clientConfig - clientConfig = &ClientConfig{ + clientConfig = &Config{ Url: "http://localhost:8080", Images: []string{"image1", "image2"}, Tag: "test-tag", @@ -334,7 +334,7 @@ func TestGenerateAppUrl(t *testing.T) { func TestSetupWatcher(t *testing.T) { // Define the input - config := &ClientConfig{ + config := &Config{ Url: "http://localhost:8080", Debug: true, } From a000a94b24cbc057678e37354ccfe48ef15a5e7f Mon Sep 17 00:00:00 2001 From: Vadim Gedz Date: Sun, 26 May 2024 00:38:46 +0300 Subject: [PATCH 2/2] chore: add methods comments --- pkg/client/client.go | 18 ++++++++++++++++++ pkg/client/config.go | 2 ++ pkg/client/utility.go | 14 ++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/pkg/client/client.go b/pkg/client/client.go index 315597c..b3d89b2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -30,6 +30,7 @@ type Watcher struct { debugMode bool } +// NewWatcher creates a new Watcher instance with the given base URL, timeout, and debug mode. func NewWatcher(baseUrl string, debugMode bool, timeout time.Duration) *Watcher { return &Watcher{ baseUrl: baseUrl, @@ -38,6 +39,8 @@ func NewWatcher(baseUrl string, debugMode bool, timeout time.Duration) *Watcher } } +// addTask adds a given task to the watcher, using either JWT or a DeployToken for authorization. +// It returns the task ID or an error. func (watcher *Watcher) addTask(task models.Task, authMethod, token string) (string, error) { // Marshal the task into JSON requestBody, err := json.Marshal(task) @@ -105,6 +108,8 @@ func (watcher *Watcher) addTask(task models.Task, authMethod, token string) (str return accepted.Id, nil } +// getTaskStatus retrieves the status of the task identified by the given ID, +// returning a TaskStatus or an error. func (watcher *Watcher) getTaskStatus(id string) (*models.TaskStatus, error) { url := fmt.Sprintf("%s/api/v1/tasks/%s", watcher.baseUrl, id) var taskStatus models.TaskStatus @@ -114,6 +119,8 @@ func (watcher *Watcher) getTaskStatus(id string) (*models.TaskStatus, error) { return &taskStatus, nil } +// getWatcherConfig retrieves the watcher's server configuration, +// returning a ServerConfig or an error. func (watcher *Watcher) getWatcherConfig() (*config.ServerConfig, error) { url := fmt.Sprintf("%s/api/v1/config", watcher.baseUrl) var serverConfig config.ServerConfig @@ -123,6 +130,8 @@ func (watcher *Watcher) getWatcherConfig() (*config.ServerConfig, error) { return &serverConfig, nil } +// waitForDeployment waits for the deployment identified by the given ID, +// performing retries if necessary, and returns an error if deployment fails. func (watcher *Watcher) waitForDeployment(id, appName, version string) error { retryCount := 0 @@ -154,6 +163,9 @@ func (watcher *Watcher) waitForDeployment(id, appName, version string) error { } } +// handleDeploymentError logs the given error, +// generates an application URL in case of deployment failure +// and exits the program with code 1. func handleDeploymentError(watcher *Watcher, task models.Task, err error) { log.Println(err) if strings.Contains(err.Error(), "The deployment has failed") { @@ -166,14 +178,20 @@ func handleDeploymentError(watcher *Watcher, task models.Task, err error) { os.Exit(1) } +// handleFatalError logs a provided error message and terminates the program with status 1. func handleFatalError(err error, message string) { log.Fatalf("%s Got the following error: %s", message, err) } +// isDeploymentOverTime checks if the deployment has exceeded the expected deployment time, +// returning a boolean value. func isDeploymentOverTime(retryCount int, retryInterval time.Duration, expectedDeploymentTime time.Duration) bool { return time.Duration(retryCount)*retryInterval > expectedDeploymentTime } +// Run initializes the client configuration, sets up the watcher, +// creates the task, adds the task to the watcher, +// waits for deployment and handles any errors in the process. func Run() { var err error diff --git a/pkg/client/config.go b/pkg/client/config.go index 11e4e64..2334ab2 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -22,6 +22,8 @@ type Config struct { Debug bool `env:"DEBUG"` } +// NewClientConfig parses the environment variables to fill a Config struct +// and returns the new instance or an error. func NewClientConfig() (*Config, error) { var config Config diff --git a/pkg/client/utility.go b/pkg/client/utility.go index afc4a5f..9e493f8 100644 --- a/pkg/client/utility.go +++ b/pkg/client/utility.go @@ -10,6 +10,8 @@ import ( "github.com/shini4i/argo-watcher/internal/models" ) +// doRequest creates a new HTTP request and sends it using the watcher's client, +// returning the response or an error. func (watcher *Watcher) doRequest(method, url string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, url, body) if err != nil { @@ -18,6 +20,8 @@ func (watcher *Watcher) doRequest(method, url string, body io.Reader) (*http.Res return watcher.client.Do(req) } +// getJSON sends a GET request to a provided URL, +// parses the JSON response and stores it in the value pointed by v. func (watcher *Watcher) getJSON(url string, v interface{}) error { resp, err := watcher.doRequest(http.MethodGet, url, nil) if err != nil { @@ -38,6 +42,8 @@ func (watcher *Watcher) getJSON(url string, v interface{}) error { return json.NewDecoder(resp.Body).Decode(v) } +// getImagesList takes a list of image names and a tag, +// then returns a list of Image structs with these properties. func getImagesList(list []string, tag string) []models.Image { var images []models.Image for _, image := range list { @@ -49,6 +55,8 @@ func getImagesList(list []string, tag string) []models.Image { return images } +// createTask takes a config struct, generates the images list and returns a Task object +// filled with the respective properties from the config. func createTask(config *Config) models.Task { images := getImagesList(config.Images, config.Tag) return models.Task{ @@ -60,6 +68,8 @@ func createTask(config *Config) models.Task { } } +// printClientConfiguration logs the current configuration of the client including the assigned images and tokens. +// It also warns if auth tokens are missing. func printClientConfiguration(watcher *Watcher, task models.Task) { fmt.Printf("Got the following configuration:\n"+ "ARGO_WATCHER_URL: %s\n"+ @@ -74,6 +84,8 @@ func printClientConfiguration(watcher *Watcher, task models.Task) { } } +// generateAppUrl fetches the watcher config and uses it to construct +// and return the URL for the Argo application. func generateAppUrl(watcher *Watcher, task models.Task) (string, error) { cfg, err := watcher.getWatcherConfig() if err != nil { @@ -86,6 +98,8 @@ func generateAppUrl(watcher *Watcher, task models.Task) (string, error) { return fmt.Sprintf("%s://%s/applications/%s", cfg.ArgoUrl.Scheme, cfg.ArgoUrl.Host, task.App), nil } +// setupWatcher takes application configuration and initializes a new Watcher instance +// with the specified parameters. func setupWatcher(config *Config) *Watcher { return NewWatcher( strings.TrimSuffix(config.Url, "/"),