diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 1f4d9bcc..15969031 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -82,6 +82,7 @@ jobs: --set piper.gitProvider.webhook.repoList={piper-e2e-test} \ --set piper.gitProvider.organization.name="rookout" \ --set image.repository=localhost:5001 \ + --set piper.argoWorkflows.server.address="${{ env.NGROK_URL }}/argo" \ --set env\[0\].name=ROOKOUT_CONTROLLER_HOST,env\[0\].value=wss://dogfood.control.rookout.com && \ sleep 20 && kubectl logs deployment/piper kubectl wait \ diff --git a/cmd/piper/piper.go b/cmd/piper/piper.go index 4b69b81e..a779295f 100644 --- a/cmd/piper/piper.go +++ b/cmd/piper/piper.go @@ -1,16 +1,15 @@ package main import ( - "log" - + rookout "github.com/Rookout/GoSDK" "github.com/rookout/piper/pkg/clients" "github.com/rookout/piper/pkg/conf" + "github.com/rookout/piper/pkg/event_handler" "github.com/rookout/piper/pkg/git_provider" "github.com/rookout/piper/pkg/server" "github.com/rookout/piper/pkg/utils" workflowHandler "github.com/rookout/piper/pkg/workflow_handler" - - rookout "github.com/Rookout/GoSDK" + "log" ) func main() { @@ -46,5 +45,11 @@ func main() { Workflows: workflows, } + err = globalClients.GitProvider.SetWebhook() + if err != nil { + panic(err) + } + + event_handler.Start(cfg, globalClients) server.Start(cfg, globalClients) } diff --git a/pkg/event_handler/github_event_notifier.go b/pkg/event_handler/github_event_notifier.go new file mode 100644 index 00000000..8c5ce11b --- /dev/null +++ b/pkg/event_handler/github_event_notifier.go @@ -0,0 +1,58 @@ +package event_handler + +import ( + "context" + "fmt" + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/rookout/piper/pkg/clients" + "github.com/rookout/piper/pkg/conf" +) + +var workflowTranslationToGithubMap = map[string]string{ + "": "pending", + "Pending": "pending", + "Running": "pending", + "Succeeded": "success", + "Failed": "failure", + "Error": "error", +} + +type githubNotifier struct { + cfg *conf.GlobalConfig + clients *clients.Clients +} + +func NewGithubEventNotifier(cfg *conf.GlobalConfig, clients *clients.Clients) EventNotifier { + return &githubNotifier{ + cfg: cfg, + clients: clients, + } +} + +func (gn *githubNotifier) Notify(ctx *context.Context, workflow *v1alpha1.Workflow) error { + fmt.Printf("Notifing workflow, %s\n", workflow.GetName()) + + repo, ok := workflow.GetLabels()["repo"] + if !ok { + return fmt.Errorf("failed get repo label for workflow: %s", workflow.GetName()) + } + commit, ok := workflow.GetLabels()["commit"] + if !ok { + return fmt.Errorf("failed get commit label for workflow: %s", workflow.GetName()) + } + + workflowLink := fmt.Sprintf("%s/workflows/%s/%s", gn.cfg.WorkflowServerConfig.ArgoAddress, gn.cfg.Namespace, workflow.GetName()) + + status, ok := workflowTranslationToGithubMap[string(workflow.Status.Phase)] + if !ok { + return fmt.Errorf("failed to translate workflow status to github stasuts for %s status: %s", workflow.GetName(), workflow.Status.Phase) + } + + message := workflow.Status.Message + err := gn.clients.GitProvider.SetStatus(ctx, &repo, &commit, &workflowLink, &status, &message) + if err != nil { + return fmt.Errorf("failed to set status for workflow %s: %s", workflow.GetName(), err) + } + + return nil +} diff --git a/pkg/event_handler/github_event_notifier_test.go b/pkg/event_handler/github_event_notifier_test.go new file mode 100644 index 00000000..8ba501aa --- /dev/null +++ b/pkg/event_handler/github_event_notifier_test.go @@ -0,0 +1,208 @@ +package event_handler + +import ( + "context" + "errors" + "github.com/rookout/piper/pkg/git_provider" + assertion "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net/http" + "testing" + + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/rookout/piper/pkg/clients" + "github.com/rookout/piper/pkg/conf" +) + +type mockGitProvider struct{} + +func (m *mockGitProvider) GetFile(ctx *context.Context, repo string, branch string, path string) (*git_provider.CommitFile, error) { + return nil, nil +} + +func (m *mockGitProvider) GetFiles(ctx *context.Context, repo string, branch string, paths []string) ([]*git_provider.CommitFile, error) { + return nil, nil +} + +func (m *mockGitProvider) ListFiles(ctx *context.Context, repo string, branch string, path string) ([]string, error) { + return nil, nil +} + +func (m *mockGitProvider) SetWebhook() error { + return nil +} + +func (m *mockGitProvider) UnsetWebhook(ctx *context.Context) error { + return nil +} + +func (m *mockGitProvider) HandlePayload(request *http.Request, secret []byte) (*git_provider.WebhookPayload, error) { + return nil, nil +} + +func (m *mockGitProvider) SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error { + return nil +} + +func TestNotify(t *testing.T) { + assert := assertion.New(t) + ctx := context.Background() + + // Define test cases + tests := []struct { + name string + workflow *v1alpha1.Workflow + wantedError error + }{ + { + name: "Succeeded workflow", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "repo": "test-repo", + "commit": "test-commit", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowSucceeded, + Message: "", + }, + }, + wantedError: nil, + }, + { + name: "Failed workflow", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "repo": "test-repo", + "commit": "test-commit", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowFailed, + Message: "something", + }, + }, + wantedError: nil, + }, + { + name: "Error workflow", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "repo": "test-repo", + "commit": "test-commit", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowError, + Message: "something", + }, + }, + wantedError: nil, + }, + { + name: "Pending workflow", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "repo": "test-repo", + "commit": "test-commit", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowPending, + Message: "something", + }, + }, + wantedError: nil, + }, + { + name: "Running workflow", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "repo": "test-repo", + "commit": "test-commit", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowRunning, + Message: "something", + }, + }, + wantedError: nil, + }, + { + name: "Missing label repo", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "commit": "test-commit", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowSucceeded, + Message: "something", + }, + }, + wantedError: errors.New("some error"), + }, + { + name: "Missing label commit", + workflow: &v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-workflow", + Labels: map[string]string{ + "repo": "test-repo", + }, + }, + Status: v1alpha1.WorkflowStatus{ + Phase: v1alpha1.WorkflowSucceeded, + Message: "something", + }, + }, + wantedError: errors.New("some error"), + }, + } + + // Create a mock configuration and clients + cfg := &conf.GlobalConfig{ + WorkflowServerConfig: conf.WorkflowServerConfig{ + ArgoAddress: "http://workflow-server", + Namespace: "test-namespace", + }, + } + clients := &clients.Clients{ + GitProvider: &mockGitProvider{}, + } + + // Create a new githubNotifier instance + gn := NewGithubEventNotifier(cfg, clients) + + // Call the Notify method + + // Run test cases + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Call the function being tested + err := gn.Notify(&ctx, test.workflow) + + // Use assert to check the equality of the error + if test.wantedError != nil { + assert.Error(err) + assert.NotNil(err) + } else { + assert.NoError(err) + assert.Nil(err) + } + }) + } +} diff --git a/pkg/event_handler/main.go b/pkg/event_handler/main.go new file mode 100644 index 00000000..36c666c8 --- /dev/null +++ b/pkg/event_handler/main.go @@ -0,0 +1,46 @@ +package event_handler + +import ( + "context" + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/rookout/piper/pkg/clients" + "github.com/rookout/piper/pkg/conf" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "log" +) + +func Start(cfg *conf.GlobalConfig, clients *clients.Clients) { + ctx := context.Background() // TODO: use global context that initialized at main + labelSelector := &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "piper.rookout.com/notified", + Operator: metav1.LabelSelectorOpExists}, + {Key: "piper.rookout.com/notified", + Operator: metav1.LabelSelectorOpNotIn, + Values: []string{ + string(v1alpha1.WorkflowSucceeded), + string(v1alpha1.WorkflowFailed), + string(v1alpha1.WorkflowError), + }}, // mean that there already completed and notified + }, + } + watcher, err := clients.Workflows.Watch(&ctx, labelSelector) + if err != nil { + log.Printf("[event handler] Failed to watch workflow error:%s", err) + return + } + + notifier := NewGithubEventNotifier(cfg, clients) + handler := &workflowEventHandler{ + Clients: clients, + Notifier: notifier, + } + go func() { + for event := range watcher.ResultChan() { + err = handler.Handle(ctx, &event) + if err != nil { + log.Printf("[event handler] failed to Handle workflow event %s", err) // ERROR + } + } + }() +} diff --git a/pkg/event_handler/types.go b/pkg/event_handler/types.go new file mode 100644 index 00000000..eebc4f15 --- /dev/null +++ b/pkg/event_handler/types.go @@ -0,0 +1,15 @@ +package event_handler + +import ( + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "golang.org/x/net/context" + "k8s.io/apimachinery/pkg/watch" +) + +type EventHandler interface { + Handle(ctx context.Context, event *watch.Event) error +} + +type EventNotifier interface { + Notify(ctx *context.Context, workflow *v1alpha1.Workflow) error +} diff --git a/pkg/event_handler/workflow_event_handler.go b/pkg/event_handler/workflow_event_handler.go new file mode 100644 index 00000000..71dd60ab --- /dev/null +++ b/pkg/event_handler/workflow_event_handler.go @@ -0,0 +1,60 @@ +package event_handler + +import ( + "fmt" + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/rookout/piper/pkg/clients" + "golang.org/x/net/context" + "k8s.io/apimachinery/pkg/watch" + "log" +) + +type workflowEventHandler struct { + Clients *clients.Clients + Notifier EventNotifier +} + +func (weh *workflowEventHandler) Handle(ctx context.Context, event *watch.Event) error { + workflow, ok := event.Object.(*v1alpha1.Workflow) + if !ok { + return fmt.Errorf( + "event object is not a Workflow object, it's kind is: %s\n", + event.DeepCopy().Object.GetObjectKind(), + ) + } + + currentPiperNotifyLabelStatus, ok := workflow.GetLabels()["piper.rookout.com/notified"] + if !ok { + return fmt.Errorf( + "workflow %s missing piper.rookout.com/notified label\n", + workflow.GetName(), + ) + } + + if currentPiperNotifyLabelStatus == string(workflow.Status.Phase) { + log.Printf( + "workflow %s already informed for %s status. skiping... \n", + workflow.GetName(), + workflow.Status.Phase, + ) //INFO + return nil + } + + err := weh.Notifier.Notify(&ctx, workflow) + if err != nil { + return fmt.Errorf("failed to Notify workflow to github, error:%s\n", err) + } + + err = weh.Clients.Workflows.UpdatePiperWorkflowLabel(&ctx, workflow.GetName(), "notified", string(workflow.Status.Phase)) + if err != nil { + return fmt.Errorf("error in workflow %s status patch: %s", workflow.GetName(), err) + } + log.Printf( + "[event handler] done with event of type: %s for worklfow: %s phase: %s message: %s\n", + event.Type, + workflow.GetName(), + workflow.Status.Phase, + workflow.Status.Message) //INFO + + return nil +} diff --git a/pkg/git_provider/github.go b/pkg/git_provider/github.go index 532b9b7b..9fc3aec7 100644 --- a/pkg/git_provider/github.go +++ b/pkg/git_provider/github.go @@ -3,6 +3,7 @@ package git_provider import ( "context" "fmt" + "github.com/rookout/piper/pkg/utils" "log" "net/http" "strings" @@ -261,3 +262,27 @@ func (c *GithubClientImpl) HandlePayload(request *http.Request, secret []byte) ( return webhookPayload, nil } + +func (c *GithubClientImpl) SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error { + if !utils.ValidateHTTPFormat(*linkURL) { + return fmt.Errorf("invalid linkURL") + } + repoStatus := &github.RepoStatus{ + State: status, // pending, success, error, or failure. + TargetURL: linkURL, + Description: utils.SPtr(fmt.Sprintf("Workflow %s %s", *status, *message)), + Context: utils.SPtr("Piper/ArgoWorkflows"), + AvatarURL: utils.SPtr("https://argoproj.github.io/argo-workflows/assets/logo.png"), + } + _, resp, err := c.client.Repositories.CreateStatus(*ctx, c.cfg.OrgName, *repo, *commit, repoStatus) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("failed to set status on repo:%s, commit:%s, API call returned %d", *repo, *commit, resp.StatusCode) + } + + log.Printf("successfully set status on repo:%s commit: %s to status: %s\n", *repo, *commit, *status) + return nil +} diff --git a/pkg/git_provider/github_test.go b/pkg/git_provider/github_test.go index 85338fd9..a1eeb015 100644 --- a/pkg/git_provider/github_test.go +++ b/pkg/git_provider/github_test.go @@ -3,6 +3,7 @@ package git_provider import ( "context" "encoding/json" + "errors" "fmt" "net/http" "testing" @@ -71,3 +72,124 @@ func TestListFiles(t *testing.T) { assert.Equal(expectedContent, actualContent) } + +func TestSetStatus(t *testing.T) { + // Prepare + ctx := context.Background() + assert := assertion.New(t) + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/test/test-repo1/statuses/test-commit", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testFormValues(t, r, values{}) + + w.WriteHeader(http.StatusCreated) + jsonBytes := []byte(`{"status": "ok"}`) + _, _ = fmt.Fprint(w, string(jsonBytes)) + }) + + c := GithubClientImpl{ + client: client, + cfg: &conf.GlobalConfig{ + GitProviderConfig: conf.GitProviderConfig{ + OrgLevelWebhook: false, + OrgName: "test", + RepoList: "test-repo1", + }, + }, + } + + // Define test cases + tests := []struct { + name string + repo *string + commit *string + linkURL *string + status *string + message *string + wantedError error + }{ + { + name: "Notify success", + repo: utils.SPtr("test-repo1"), + commit: utils.SPtr("test-commit"), + linkURL: utils.SPtr("https://argo"), + status: utils.SPtr("success"), + message: utils.SPtr(""), + wantedError: nil, + }, + { + name: "Notify pending", + repo: utils.SPtr("test-repo1"), + commit: utils.SPtr("test-commit"), + linkURL: utils.SPtr("https://argo"), + status: utils.SPtr("pending"), + message: utils.SPtr(""), + wantedError: nil, + }, + { + name: "Notify error", + repo: utils.SPtr("test-repo1"), + commit: utils.SPtr("test-commit"), + linkURL: utils.SPtr("https://argo"), + status: utils.SPtr("error"), + message: utils.SPtr("some message"), + wantedError: nil, + }, + { + name: "Notify failure", + repo: utils.SPtr("test-repo1"), + commit: utils.SPtr("test-commit"), + linkURL: utils.SPtr("https://argo"), + status: utils.SPtr("failure"), + message: utils.SPtr(""), + wantedError: nil, + }, + { + name: "Non managed repo", + repo: utils.SPtr("non-existing-repo"), + commit: utils.SPtr("test-commit"), + linkURL: utils.SPtr("https://argo"), + status: utils.SPtr("error"), + message: utils.SPtr(""), + wantedError: errors.New("some error"), + }, + { + name: "Non existing commit", + repo: utils.SPtr("test-repo1"), + commit: utils.SPtr("not-exists"), + linkURL: utils.SPtr("https://argo"), + status: utils.SPtr("error"), + message: utils.SPtr(""), + wantedError: errors.New("some error"), + }, + { + name: "Wrong URL", + repo: utils.SPtr("test-repo1"), + commit: utils.SPtr("test-commit"), + linkURL: utils.SPtr("argo"), + status: utils.SPtr("error"), + message: utils.SPtr(""), + wantedError: errors.New("some error"), + }, + } + // Run test cases + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + // Call the function being tested + err := c.SetStatus(&ctx, test.repo, test.commit, test.linkURL, test.status, test.message) + + // Use assert to check the equality of the error + if test.wantedError != nil { + assert.Error(err) + assert.NotNil(err) + } else { + assert.NoError(err) + assert.Nil(err) + } + }) + } + +} diff --git a/pkg/git_provider/types.go b/pkg/git_provider/types.go index 4c05b86f..c9f1d9b3 100644 --- a/pkg/git_provider/types.go +++ b/pkg/git_provider/types.go @@ -30,4 +30,5 @@ type Client interface { SetWebhook() error UnsetWebhook(ctx *context.Context) error HandlePayload(request *http.Request, secret []byte) (*WebhookPayload, error) + SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error } diff --git a/pkg/utils/common.go b/pkg/utils/common.go index 51612942..89d30708 100644 --- a/pkg/utils/common.go +++ b/pkg/utils/common.go @@ -3,6 +3,7 @@ package utils import ( "encoding/json" "fmt" + "regexp" "strings" "gopkg.in/yaml.v3" @@ -120,3 +121,9 @@ func SPtr(str string) *string { func BPtr(b bool) *bool { return &b } + +func ValidateHTTPFormat(input string) bool { + regex := `^(https?://)([\w-]+(\.[\w-]+)*)(:\d+)?(/[\w-./?%&=]*)?$` + match, _ := regexp.MatchString(regex, input) + return match +} diff --git a/pkg/utils/common_test.go b/pkg/utils/common_test.go index dd57989c..96ec6140 100644 --- a/pkg/utils/common_test.go +++ b/pkg/utils/common_test.go @@ -1,124 +1,134 @@ package utils import ( - "github.com/stretchr/testify/assert" + assertion "github.com/stretchr/testify/assert" "testing" ) func TestListContains(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: subList is empty list1 := []string{"apple", "banana", "orange"} subList1 := []string{} expectedResult1 := true result := ListContains(subList1, list1) - assert.Equal(t, expectedResult1, result) + assert.Equal(expectedResult1, result) // Test Case 2: subList is a subset of list list2 := []string{"apple", "banana", "orange"} subList2 := []string{"banana"} expectedResult2 := true result = ListContains(subList2, list2) - assert.Equal(t, expectedResult2, result) + assert.Equal(expectedResult2, result) // Test Case 3: subList is not a subset of list list3 := []string{"apple", "banana", "orange"} subList3 := []string{"banana", "mango"} expectedResult3 := false result = ListContains(subList3, list3) - assert.Equal(t, expectedResult3, result) + assert.Equal(expectedResult3, result) // Test Case 4: subList is longer than list list4 := []string{"apple", "banana", "orange"} subList4 := []string{"apple", "banana", "orange", "mango"} expectedResult4 := false result = ListContains(subList4, list4) - assert.Equal(t, expectedResult4, result) + assert.Equal(expectedResult4, result) } func TestIsElementExists(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: Element exists in the list list1 := []string{"apple", "banana", "orange"} element1 := "banana" expectedResult1 := true result := IsElementExists(list1, element1) - assert.Equal(t, expectedResult1, result) + assert.Equal(expectedResult1, result) // Test Case 2: Element does not exist in the list list2 := []string{"apple", "banana", "orange"} element2 := "mango" expectedResult2 := false result = IsElementExists(list2, element2) - assert.Equal(t, expectedResult2, result) + assert.Equal(expectedResult2, result) // Test Case 3: Empty list list3 := []string{} element3 := "apple" expectedResult3 := false result = IsElementExists(list3, element3) - assert.Equal(t, expectedResult3, result) + assert.Equal(expectedResult3, result) } func TestIsElementMatch(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: Element matches "*" wildcard elements1 := []string{"apple", "banana", "orange", "*"} element1 := "mango" expectedResult1 := true result := IsElementMatch(element1, elements1) - assert.Equal(t, expectedResult1, result) + assert.Equal(expectedResult1, result) // Test Case 2: Element matches a specific element in the list elements2 := []string{"apple", "banana", "orange"} element2 := "banana" expectedResult2 := true result = IsElementMatch(element2, elements2) - assert.Equal(t, expectedResult2, result) + assert.Equal(expectedResult2, result) // Test Case 3: Element does not match any element in the list elements3 := []string{"apple", "banana", "orange"} element3 := "mango" expectedResult3 := false result = IsElementMatch(element3, elements3) - assert.Equal(t, expectedResult3, result) + assert.Equal(expectedResult3, result) // Test Case 4: Element matches "*" wildcard but is not present in the list elements4 := []string{"apple", "banana", "orange", "*"} element4 := "grape" expectedResult4 := true result = IsElementMatch(element4, elements4) - assert.Equal(t, expectedResult4, result) + assert.Equal(expectedResult4, result) // Test Case 5: Empty list elements5 := []string{} element5 := "apple" expectedResult5 := false result = IsElementMatch(element5, elements5) - assert.Equal(t, expectedResult5, result) + assert.Equal(expectedResult5, result) } func TestAddPrefixToList(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: Add prefix to each item in the list list1 := []string{"apple", "banana", "orange"} prefix1 := "fruit_" expectedResult1 := []string{"fruit_apple", "fruit_banana", "fruit_orange"} result1 := AddPrefixToList(list1, prefix1) - assert.Equal(t, expectedResult1, result1) + assert.Equal(expectedResult1, result1) // Test Case 2: Add empty prefix to each item in the list list2 := []string{"apple", "banana", "orange"} prefix2 := "" expectedResult2 := []string{"apple", "banana", "orange"} result2 := AddPrefixToList(list2, prefix2) - assert.Equal(t, expectedResult2, result2) + assert.Equal(expectedResult2, result2) // Test Case 3: Empty list list3 := []string{} prefix3 := "prefix_" expectedResult3 := []string{} result3 := AddPrefixToList(list3, prefix3) - assert.Equal(t, expectedResult3, result3) + assert.Equal(expectedResult3, result3) } func TestStringToMap(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: Valid string with key-value pairs str1 := "key1:value1, key2:value2, key3:value3" expectedResult1 := map[string]string{ @@ -128,7 +138,7 @@ func TestStringToMap(t *testing.T) { } result1 := StringToMap(str1) for key, value := range expectedResult1 { - assert.Equal(t, value, result1[key]) + assert.Equal(value, result1[key]) } // Test Case 2: Valid string with empty key-value pairs @@ -140,16 +150,18 @@ func TestStringToMap(t *testing.T) { } result2 := StringToMap(str2) for key, value := range expectedResult2 { - assert.Equal(t, value, result2[key]) + assert.Equal(value, result2[key]) } // Test Case 3: Empty string str3 := "" result3 := StringToMap(str3) - assert.Empty(t, result3) + assert.Empty(result3) } func TestConvertYAMLListToJSONList(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: Valid YAML list yamlString1 := ` - name: John @@ -159,15 +171,15 @@ func TestConvertYAMLListToJSONList(t *testing.T) { ` expectedJSON1 := `[{"age":30,"name":"John"},{"age":25,"name":"Jane"}]` resultJSON1, err := ConvertYAMLListToJSONList(yamlString1) - assert.NoError(t, err) - assert.JSONEq(t, expectedJSON1, string(resultJSON1)) + assert.NoError(err) + assert.JSONEq(expectedJSON1, string(resultJSON1)) // Test Case 2: Empty YAML list yamlString2 := `[]` expectedJSON2 := `[]` resultJSON2, err := ConvertYAMLListToJSONList(yamlString2) - assert.NoError(t, err) - assert.JSONEq(t, expectedJSON2, string(resultJSON2)) + assert.NoError(err) + assert.JSONEq(expectedJSON2, string(resultJSON2)) // Test Case 3: Invalid YAML format yamlString3 := ` @@ -175,11 +187,13 @@ name: John age: 30 ` resultJSON3, err := ConvertYAMLListToJSONList(yamlString3) - assert.Error(t, err) - assert.Nil(t, resultJSON3) + assert.Error(err) + assert.Nil(resultJSON3) } func TestConvertYAMToJSON(t *testing.T) { + assert := assertion.New(t) + // Test Case 1: Valid YAML yamlString1 := []byte(` name: John @@ -187,15 +201,15 @@ age: 30 `) expectedJSON1 := `{"age":30,"name":"John"}` resultJSON1, err := ConvertYAMLToJSON(yamlString1) - assert.NoError(t, err) - assert.JSONEq(t, expectedJSON1, string(resultJSON1)) + assert.NoError(err) + assert.JSONEq(expectedJSON1, string(resultJSON1)) // Test Case 2: Empty YAML yamlString2 := []byte("") expectedJSON2 := `{}` resultJSON2, err := ConvertYAMLToJSON(yamlString2) - assert.NoError(t, err) - assert.JSONEq(t, expectedJSON2, string(resultJSON2)) + assert.NoError(err) + assert.JSONEq(expectedJSON2, string(resultJSON2)) // Test Case 3: Invalid YAML format yamlString3 := []byte(` @@ -203,16 +217,89 @@ age: 30 age: 30 `) resultJSON3, err := ConvertYAMLToJSON(yamlString3) - assert.Error(t, err) - assert.Nil(t, resultJSON3) + assert.Error(err) + assert.Nil(resultJSON3) } func TestSPtr(t *testing.T) { + assert := assertion.New(t) + sPtr := SPtr("test") - assert.Equal(t, *sPtr, "test") + assert.Equal(*sPtr, "test") } func TestBPtr(t *testing.T) { + assert := assertion.New(t) + bPtr := BPtr(false) - assert.Equal(t, *bPtr, false) + assert.Equal(*bPtr, false) +} + +func TestValidateHTTPFormat(t *testing.T) { + assert := assertion.New(t) + + tests := []struct { + name string + url string + wantedResult bool + }{ + // Valid URLs + { + name: "Valid HTTP URL", + url: "http://example.com", + wantedResult: true, + }, + { + name: "Valid HTTPS URL", + url: "https://example.com", + wantedResult: true, + }, + { + name: "Valid HTTP URL with Port", + url: "http://example.com:8080", + wantedResult: true, + }, + { + name: "Valid HTTPS URL with Path", + url: "https://example.com/path", + wantedResult: true, + }, + { + name: "Valid HTTP URL without TLD", + url: "http://example", + wantedResult: true, + }, + + // Invalid URLs + { + name: "Invalid URL: Missing Protocol", + url: "example.com", + wantedResult: false, + }, + { + name: "Invalid URL: Unsupported Protocol", + url: "ftp://example.com", + wantedResult: false, + }, + { + name: "Invalid URL: Missing Slash", + url: "http:/example.com", + wantedResult: false, + }, + { + name: "Invalid URL: Missing Port Number", + url: "https://example.com:", + wantedResult: false, + }, + } + + // Run test cases + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Call the function being tested + gotResult := ValidateHTTPFormat(test.url) + assert.Equal(test.wantedResult, gotResult) + }) + } + } diff --git a/pkg/webhook_handler/webhook_handler_test.go b/pkg/webhook_handler/webhook_handler_test.go index f992960b..d9c5098e 100644 --- a/pkg/webhook_handler/webhook_handler_test.go +++ b/pkg/webhook_handler/webhook_handler_test.go @@ -80,6 +80,10 @@ func (m *MockGitProvider) HandlePayload(request *http.Request, secret []byte) (* return nil, nil } +func (m *MockGitProvider) SetStatus(ctx *context.Context, repo *string, commit *string, linkURL *string, status *string, message *string) error { + return nil +} + func TestPrepareBatchForMatchingTriggers(t *testing.T) { assert := assertion.New(t) ctx := context.Background() diff --git a/pkg/workflow_handler/types.go b/pkg/workflow_handler/types.go index b9c4828c..b8e94e7f 100644 --- a/pkg/workflow_handler/types.go +++ b/pkg/workflow_handler/types.go @@ -4,6 +4,8 @@ import ( "context" "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" "github.com/rookout/piper/pkg/common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" ) type WorkflowsClient interface { @@ -14,4 +16,6 @@ type WorkflowsClient interface { Lint(wf *v1alpha1.Workflow) error Submit(ctx *context.Context, wf *v1alpha1.Workflow) error HandleWorkflowBatch(ctx *context.Context, workflowsBatch *common.WorkflowsBatch) error + Watch(ctx *context.Context, labelSelector *metav1.LabelSelector) (watch.Interface, error) + UpdatePiperWorkflowLabel(ctx *context.Context, workflowName string, label string, value string) error } diff --git a/pkg/workflow_handler/workflows.go b/pkg/workflow_handler/workflows.go index 552a9658..1ec82e9b 100644 --- a/pkg/workflow_handler/workflows.go +++ b/pkg/workflow_handler/workflows.go @@ -2,10 +2,14 @@ package workflow_handler import ( "context" + "encoding/json" "fmt" "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" wfClientSet "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned" + "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" "log" "github.com/rookout/piper/pkg/common" @@ -93,10 +97,11 @@ func (wfc *WorkflowsClientImpl) CreateWorkflow(spec *v1alpha1.WorkflowSpec, work GenerateName: ConvertToValidString(workflowsBatch.Payload.Repo + "-" + workflowsBatch.Payload.Branch + "-"), Namespace: wfc.cfg.Namespace, Labels: map[string]string{ - "repo": ConvertToValidString(workflowsBatch.Payload.Repo), - "branch": ConvertToValidString(workflowsBatch.Payload.Branch), - "user": ConvertToValidString(workflowsBatch.Payload.User), - "commit": ConvertToValidString(workflowsBatch.Payload.Commit), + "piper.rookout.com/notified": "false", + "repo": ConvertToValidString(workflowsBatch.Payload.Repo), + "branch": ConvertToValidString(workflowsBatch.Payload.Branch), + "user": ConvertToValidString(workflowsBatch.Payload.User), + "commit": ConvertToValidString(workflowsBatch.Payload.Commit), }, }, Spec: *spec, @@ -144,6 +149,7 @@ func (wfc *WorkflowsClientImpl) Submit(ctx *context.Context, wf *v1alpha1.Workfl if err != nil { return err } + return nil } @@ -196,3 +202,37 @@ func (wfc *WorkflowsClientImpl) HandleWorkflowBatch(ctx *context.Context, workfl log.Printf("submit workflow for branch %s repo %s commit %s", workflowsBatch.Payload.Branch, workflowsBatch.Payload.Repo, workflowsBatch.Payload.Commit) return nil } + +func (wfc *WorkflowsClientImpl) Watch(ctx *context.Context, labelSelector *metav1.LabelSelector) (watch.Interface, error) { + workflowsClient := wfc.clientSet.ArgoprojV1alpha1().Workflows(wfc.cfg.Namespace) + opts := v1.ListOptions{ + Watch: true, + LabelSelector: metav1.FormatLabelSelector(labelSelector), + } + watcher, err := workflowsClient.Watch(*ctx, opts) + if err != nil { + return nil, err + } + + return watcher, nil +} + +func (wfc *WorkflowsClientImpl) UpdatePiperWorkflowLabel(ctx *context.Context, workflowName string, label string, value string) error { + workflowsClient := wfc.clientSet.ArgoprojV1alpha1().Workflows(wfc.cfg.Namespace) + + patch, err := json.Marshal(map[string]interface{}{"metadata": metav1.ObjectMeta{ + Labels: map[string]string{ + fmt.Sprintf("piper.rookout.com/%s", label): value, + }, + }}) + if err != nil { + return err + } + _, err = workflowsClient.Patch(*ctx, workflowName, types.MergePatchType, patch, v1.PatchOptions{}) + if err != nil { + return err + } + + fmt.Printf("workflow %s labels piper.rookout.com/%s updated to %s\n", workflowName, label, value) + return nil +} diff --git a/pkg/workflow_handler/workflows_test.go b/pkg/workflow_handler/workflows_test.go index 9f47bc35..2e40d236 100644 --- a/pkg/workflow_handler/workflows_test.go +++ b/pkg/workflow_handler/workflows_test.go @@ -150,10 +150,11 @@ func TestCreateWorkflow(t *testing.T) { assert.Equal("my-repo-my-branch-", workflow.ObjectMeta.GenerateName) assert.Equal(wfcImpl.cfg.Namespace, workflow.ObjectMeta.Namespace) assert.Equal(map[string]string{ - "repo": "my-repo", - "branch": "my-branch", - "user": "my-user", - "commit": "my-commit", + "piper.rookout.com/notified": "false", + "repo": "my-repo", + "branch": "my-branch", + "user": "my-user", + "commit": "my-commit", }, workflow.ObjectMeta.Labels) // Assert that the workflow's Spec is assigned correctly