Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(client): add support for additional configuration #306

Merged
merged 2 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)

var (
clientConfig *ClientConfig
clientConfig *Config
)

type Watcher struct {
Expand All @@ -30,6 +30,7 @@
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,
Expand All @@ -38,6 +39,8 @@
}
}

// 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)
Expand Down Expand Up @@ -105,6 +108,8 @@
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
Expand All @@ -114,6 +119,8 @@
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
Expand All @@ -123,7 +130,11 @@
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

for {
taskInfo, err := watcher.getTaskStatus(id)
if err != nil {
Expand All @@ -134,8 +145,13 @@
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)

Check warning on line 154 in pkg/client/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/client/client.go#L148-L154

Added lines #L148 - L154 were not covered by tests
case models.StatusAppNotFoundMessage:
return fmt.Errorf("Application %s does not exist.\n%s", appName, taskInfo.StatusReason)
case models.StatusArgoCDUnavailableMessage:
Expand All @@ -147,6 +163,9 @@
}
}

// 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") {
Expand All @@ -159,10 +178,20 @@
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

Expand Down
24 changes: 24 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
32 changes: 18 additions & 14 deletions pkg/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ 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
// 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

if err := envConfig.Parse(&config); err != nil {
return nil, err
Expand Down
18 changes: 16 additions & 2 deletions pkg/client/utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -49,7 +55,9 @@ func getImagesList(list []string, tag string) []models.Image {
return images
}

func createTask(config *ClientConfig) models.Task {
// 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{
App: config.App,
Expand All @@ -60,6 +68,8 @@ func createTask(config *ClientConfig) 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"+
Expand All @@ -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 {
Expand All @@ -86,7 +98,9 @@ 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 {
// 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, "/"),
config.Debug,
Expand Down
8 changes: 4 additions & 4 deletions pkg/client/utility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
}
Expand Down
Loading