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): small refactoring and additional debug information #210

Merged
merged 10 commits into from
Oct 19, 2023
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