Skip to content

Commit

Permalink
chore(client): small refactoring and additional debug information (#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
shini4i authored Oct 19, 2023
1 parent f5e01cc commit 0c50786
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 95 deletions.
4 changes: 2 additions & 2 deletions cmd/argo-watcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ var errorInvalidMode = errors.New("invalid mode")
func runWatcher(serverFlag, clientFlag bool) error {
// start server if requested
if serverFlag && !clientFlag {
serverWatcher()
runServer()
return nil
}

// start client if requested
if clientFlag && !serverFlag {
client.ClientWatcher()
client.Run()
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/argo-watcher/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func initLogs(logLevel string, logFormat string) {
}
}

func serverWatcher() {
func runServer() {
// initialize serverConfig
serverConfig, err := config.NewServerConfig()
if err != nil {
Expand Down
47 changes: 47 additions & 0 deletions internal/helpers/helpers.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package helpers

import (
"fmt"
"net/http"
"net/http/httputil"
"strings"
)

// Contains is a simple utility function that checks if a given string (s) exists in a slice of strings (slice).
// It iterates through the elements in the slice and returns true if it finds a match,
// indicating that the string exists in the slice; otherwise, it returns false, indicating that the string is not present.
func Contains(slice []string, s string) bool {
for _, item := range slice {
if item == s {
Expand All @@ -9,6 +19,10 @@ func Contains(slice []string, s string) bool {
return false
}

// ImagesContains checks whether a given list of images contains a specific image.
// It takes into account an optional registry proxy and checks both the image with
// and without the proxy to ensure compatibility with mutating webhooks.
// The function returns true if the image is found in the list, considering the proxy if specified; otherwise, it returns false.
func ImagesContains(images []string, image string, registryProxy string) bool {
if registryProxy != "" {
imageWithProxy := registryProxy + "/" + image
Expand All @@ -19,3 +33,36 @@ func ImagesContains(images []string, image string, registryProxy string) bool {
return Contains(images, image)
}
}

// CurlCommandFromRequest generates a cURL command string from an HTTP request,
// including the request method, headers, request body, and URL.
// It handles any errors during the process and returns the formatted cURL command or an error if encountered.
func CurlCommandFromRequest(request *http.Request) (string, error) {
clonedRequest, err := httputil.DumpRequest(request, true)
if err != nil {
return "", err
}

cmd := "curl -X " + request.Method

// Iterate over request headers and add them to the cURL command
for key, values := range request.Header {
for _, value := range values {
cmd += fmt.Sprintf(" -H '%s: %s'", key, value)
}
}

// Add request body to cURL command
if len(clonedRequest) > 0 {
// Exclude the request line and headers when adding the body
body := string(clonedRequest[strings.Index(string(clonedRequest), "\r\n\r\n")+4:])
if len(body) > 0 {
cmd += " -d '" + body + "'"
}
}

// Add request URL to cURL command
cmd += " '" + request.URL.String() + "'"

return cmd, nil
}
34 changes: 34 additions & 0 deletions internal/helpers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package helpers

import (
"fmt"
"net/http"
"sort"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -57,3 +60,34 @@ func TestImageContains(t *testing.T) {
assert.Equal(t, test.expected, ImagesContains(test.images, test.image, test.registryProxy), testErrorMsg)
}
}

func TestCurlCommandFromRequest(t *testing.T) {
// Create a sample HTTP request with a non-empty request body
requestBody := `{"key": "value"}`
request, _ := http.NewRequest("POST", "https://example.com/api", strings.NewReader(requestBody))
request.Header.Add("Content-Type", "application/json")
request.Header.Add("Authorization", "Bearer Token123")
request.Header.Add("X-Custom-Header", "CustomValue")

// Create the expected cURL command
expectedCurl := `curl -X POST -H 'Authorization: Bearer Token123' -H 'Content-Type: application/json' -H 'X-Custom-Header: CustomValue' -d '{"key": "value"}' 'https://example.com/api'`

// Call the function to get the actual cURL command
actualCurl, err := CurlCommandFromRequest(request)
assert.NoError(t, err)

// Split the cURL commands by space
expectedParts := strings.Fields(expectedCurl)
actualParts := strings.Fields(actualCurl)

// Sort the headers alphabetically, excluding the first part (curl command and method)
sort.Strings(expectedParts[3:])
sort.Strings(actualParts[3:])

// Reconstruct the cURL commands with sorted headers
sortedExpectedCurl := strings.Join(expectedParts, " ")
sortedActualCurl := strings.Join(actualParts, " ")

// Compare the expected and actual cURL commands
assert.Equal(t, sortedExpectedCurl, sortedActualCurl)
}
2 changes: 1 addition & 1 deletion internal/models/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Task struct {
Images []Image `json:"images" binding:"required"`
Status string `json:"status,omitempty"`
StatusReason string `json:"status_reason,omitempty"`
Validated bool
Validated bool `json:"validated,omitempty"`
}

// ListImages returns a list of strings representing the images of the task.
Expand Down
147 changes: 85 additions & 62 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
Expand All @@ -12,39 +13,55 @@ import (
"strings"
"time"

"github.com/shini4i/argo-watcher/internal/helpers"

"github.com/shini4i/argo-watcher/internal/models"
)

type Watcher struct {
baseUrl string
client *http.Client
baseUrl string
client *http.Client
debugMode bool
}

var (
tag = os.Getenv("IMAGE_TAG")
tag = os.Getenv("IMAGE_TAG")
debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
)

func (watcher *Watcher) addTask(task models.Task, token string) string {
body, err := json.Marshal(task)
func (watcher *Watcher) addTask(task models.Task, token string) (string, error) {
// Marshal the task into JSON
requestBody, err := json.Marshal(task)
if err != nil {
panic(err)
return "", err
}

url := fmt.Sprintf("%s/api/v1/tasks", watcher.baseUrl)
request, err := http.NewRequest("POST", url, bytes.NewBuffer(body))

// Create a new HTTP request with the JSON responseBody
request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestBody))
if err != nil {
panic(err)
return "", err
}

request.Header.Set("Content-Type", "application/json; charset=UTF-8")

// Set the deploy token header if provided
if token != "" {
request.Header.Set("ARGO_WATCHER_DEPLOY_TOKEN", token)
}

// Print the equivalent cURL command for troubleshooting
if curlCommand, err := helpers.CurlCommandFromRequest(request); err != nil {
log.Printf("Couldn't get cURL command. Got the following error: %s", err)
} else if watcher.debugMode {
log.Printf("Adding task to argo-watcher. Equivalent cURL command: %s\n", curlCommand)
}

// Send the HTTP request
response, err := watcher.client.Do(request)
if err != nil {
panic(err)
return "", err
}

defer func(Body io.ReadCloser) {
Expand All @@ -54,37 +71,36 @@ func (watcher *Watcher) addTask(task models.Task, token string) string {
}
}(response.Body)

body, err = io.ReadAll(response.Body)
responseBody, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
return "", err
}

if response.StatusCode != 202 {
fmt.Printf("Something went wrong on argo-watcher side. Got the following response code %d\n", response.StatusCode)
fmt.Printf("Body: %s\n", string(body))
os.Exit(1)
// Check the HTTP status code for success
if response.StatusCode != http.StatusAccepted {
errMsg := fmt.Sprintf("Something went wrong on argo-watcher side. Got the following response code %d", response.StatusCode)
return "", errors.New(errMsg)
}

var accepted models.TaskStatus
err = json.Unmarshal(body, &accepted)
err = json.Unmarshal(responseBody, &accepted)
if err != nil {
panic(err)
return "", err
}

return accepted.Id
return accepted.Id, nil
}

func (watcher *Watcher) getTaskStatus(id string) *models.TaskStatus {
func (watcher *Watcher) getTaskStatus(id string) (*models.TaskStatus, error) {
url := fmt.Sprintf("%s/api/v1/tasks/%s", watcher.baseUrl, id)
request, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Print(err)
os.Exit(1)
return nil, err
}

response, err := watcher.client.Do(request)
if err != nil {
panic(err)
return nil, err
}

defer func(Body io.ReadCloser) {
Expand All @@ -94,21 +110,43 @@ func (watcher *Watcher) getTaskStatus(id string) *models.TaskStatus {
}
}(response.Body)

body, _ := io.ReadAll(response.Body)

if response.StatusCode != 200 {
fmt.Printf("Received non 200 status code (%d)\n", response.StatusCode)
fmt.Printf("Body: %s\n", string(body))
os.Exit(1)
if response.StatusCode != http.StatusOK {
log.Printf("Received non-200 status code (%d)\n", response.StatusCode)
body, _ := io.ReadAll(response.Body)
log.Printf("Body: %s\n", string(body))
return nil, fmt.Errorf("received non-200 status code: %d", response.StatusCode)
}

var taskStatus models.TaskStatus
err = json.Unmarshal(body, &taskStatus)
if err != nil {
panic(err)
if err := json.NewDecoder(response.Body).Decode(&taskStatus); err != nil {
return nil, err
}

return &taskStatus
return &taskStatus, nil
}

func (watcher *Watcher) waitForDeployment(id, appName string) error {
for {
taskInfo, err := watcher.getTaskStatus(id)
if err != nil {
return err
}

switch taskInfo.Status {
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)
case models.StatusAppNotFoundMessage:
return fmt.Errorf("Application %s does not exist.\n%s", appName, taskInfo.StatusReason)
case models.StatusArgoCDUnavailableMessage:
return fmt.Errorf("ArgoCD is unavailable. Please investigate.\n%s", taskInfo.StatusReason)
case models.StatusDeployedMessage:
log.Printf("The deployment of %s version is done.\n", tag)
return nil
}
}
}

func getImagesList() []models.Image {
Expand All @@ -122,12 +160,13 @@ func getImagesList() []models.Image {
return images
}

func ClientWatcher() {
func Run() {
images := getImagesList()

watcher := Watcher{
baseUrl: strings.TrimSuffix(os.Getenv("ARGO_WATCHER_URL"), "/"),
client: &http.Client{},
baseUrl: strings.TrimSuffix(os.Getenv("ARGO_WATCHER_URL"), "/"),
client: &http.Client{},
debugMode: debug,
}

task := models.Task{
Expand All @@ -137,11 +176,9 @@ func ClientWatcher() {
Images: images,
}

debug, _ := strconv.ParseBool(os.Getenv("DEBUG"))

deployToken := os.Getenv("ARGO_WATCHER_DEPLOY_TOKEN")

if debug {
if watcher.debugMode {
fmt.Printf("Got the following configuration:\n"+
"ARGO_WATCHER_URL: %s\n"+
"ARGO_APP: %s\n"+
Expand All @@ -155,33 +192,19 @@ func ClientWatcher() {
}
}

fmt.Printf("Waiting for %s app to be running on %s version.\n", task.App, tag)
log.Printf("Waiting for %s app to be running on %s version.\n", task.App, tag)

id := watcher.addTask(task, deployToken)
id, err := watcher.addTask(task, deployToken)
if err != nil {
log.Printf("Couldn't add task. Got the following error: %s", err)
os.Exit(1)
}

// Giving Argo-Watcher some time to process the task
time.Sleep(5 * time.Second)

loop:
for {
switch taskInfo := watcher.getTaskStatus(id); taskInfo.Status {
case models.StatusFailedMessage:
fmt.Println("The deployment has failed, please check logs.")
fmt.Println(taskInfo.StatusReason)
os.Exit(1)
case models.StatusInProgressMessage:
fmt.Println("Application deployment is in progress...")
time.Sleep(15 * time.Second)
case models.StatusAppNotFoundMessage:
fmt.Printf("Application %s does not exist.\n", task.App)
fmt.Println(taskInfo.StatusReason)
os.Exit(1)
case models.StatusArgoCDUnavailableMessage:
fmt.Println("ArgoCD is unavailable. Please investigate.")
fmt.Println(taskInfo.StatusReason)
os.Exit(1)
case models.StatusDeployedMessage:
fmt.Printf("The deployment of %s version is done.\n", tag)
break loop
}
if err := watcher.waitForDeployment(id, task.App); err != nil {
log.Println(err)
os.Exit(1)
}
}
Loading

0 comments on commit 0c50786

Please sign in to comment.