diff --git a/nexus3/nexus.go b/nexus3/nexus.go index d8cd858..3740354 100644 --- a/nexus3/nexus.go +++ b/nexus3/nexus.go @@ -9,6 +9,7 @@ import ( "github.com/datadrivers/go-nexus-client/nexus3/pkg/readonly" "github.com/datadrivers/go-nexus-client/nexus3/pkg/repository" "github.com/datadrivers/go-nexus-client/nexus3/pkg/security" + "github.com/datadrivers/go-nexus-client/nexus3/pkg/task" ) const ( @@ -33,6 +34,7 @@ type NexusClient struct { RoutingRule *RoutingRuleService Script *ScriptService Security *security.SecurityService + Task *task.TaskService } // NewClient returns an instance of client that implements the Client interface @@ -50,5 +52,6 @@ func NewClient(config client.Config) *NexusClient { RoutingRule: NewRoutingRuleService(client), Script: NewScriptService(client), Security: security.NewSecurityService(client), + Task: task.NewTaskService(client), } } diff --git a/nexus3/pkg/task/service.go b/nexus3/pkg/task/service.go new file mode 100644 index 0000000..6782f54 --- /dev/null +++ b/nexus3/pkg/task/service.go @@ -0,0 +1,151 @@ +package task + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/datadrivers/go-nexus-client/nexus3/pkg/tools" + "github.com/datadrivers/go-nexus-client/nexus3/schema" + "net/http" + "net/url" + + "github.com/datadrivers/go-nexus-client/nexus3/pkg/client" + "github.com/datadrivers/go-nexus-client/nexus3/schema/task" +) + +const ( + taskAPIEndpoint = client.BasePath + "v1/tasks" +) + +var ( + ErrTaskNotRunning = errors.New("task is not currently running") +) + +type TaskService client.Service + +func NewTaskService(c *client.Client) *TaskService { + return &TaskService{Client: c} +} + +func (s *TaskService) ListTasks(taskType *string, continuationToken *string) ([]task.Task, *string, error) { + q := url.Values{} + if taskType != nil { + q.Set("type", *taskType) + } + if continuationToken != nil { + q.Set("continuationToken", *continuationToken) + } + + body, resp, err := s.Client.Get(fmt.Sprintf("%s?%s", taskAPIEndpoint, q.Encode()), nil) + if err != nil { + return nil, nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("could not list task: HTTP: %d, %s", resp.StatusCode, string(body)) + } + + var result schema.PaginationResult[task.Task] + if err := json.Unmarshal(body, &result); err != nil { + return nil, nil, fmt.Errorf("could not unmarshal tasks: %v", err) + } + + return result.Items, result.ContinuationToken, nil +} + +func (s *TaskService) GetTask(id string) (*task.Task, error) { + body, resp, err := s.Client.Get(fmt.Sprintf("%s/%s", taskAPIEndpoint, id), nil) + if err != nil { + return nil, err + } + switch resp.StatusCode { + case http.StatusOK: + var t task.Task + if err := json.Unmarshal(body, &t); err != nil { + return nil, fmt.Errorf("could not unmarshal task: %v", err) + } + return &t, nil + case http.StatusNotFound: + return nil, nil + default: + return nil, fmt.Errorf("could not get task '%s': HTTP: %d, %s", id, resp.StatusCode, string(body)) + } +} + +func (s *TaskService) RunTask(id string) error { + body, resp, err := s.Client.Post(fmt.Sprintf("%s/%s/run", taskAPIEndpoint, id), nil) + if err != nil { + return err + } + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("could not run task '%s': HTTP: %d, %s", id, resp.StatusCode, string(body)) + } + return nil +} + +func (s *TaskService) StopTask(id string) error { + body, resp, err := s.Client.Post(fmt.Sprintf("%s/%s/stop", taskAPIEndpoint, id), nil) + if err != nil { + return err + } + switch resp.StatusCode { + case http.StatusNoContent: + return nil + case http.StatusConflict: + return ErrTaskNotRunning + default: + return fmt.Errorf("could not stop task '%s': HTTP: %d, %s", id, resp.StatusCode, string(body)) + } +} + +func (s *TaskService) CreateTask(newTask *task.TaskCreateStruct) (*task.Task, error) { + ioReader, err := tools.JsonMarshalInterfaceToIOReader(newTask) + if err != nil { + return nil, err + } + body, resp, err := s.Client.Post(taskAPIEndpoint, ioReader) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("could not create task: HTTP: %d, %s", resp.StatusCode, string(body)) + } + + var createdTask task.Task + if err := json.Unmarshal(body, &createdTask); err != nil { + return nil, fmt.Errorf("could not unmarshal created task: %v", err) + } + + return &createdTask, nil +} + +func (s *TaskService) UpdateTask(id string, updatedTask *task.TaskCreateStruct) error { + ioReader, err := tools.JsonMarshalInterfaceToIOReader(updatedTask) + if err != nil { + return err + } + + body, resp, err := s.Client.Put(fmt.Sprintf("%s/%s", taskAPIEndpoint, id), ioReader) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("could not update task '%s': HTTP: %d, %s", id, resp.StatusCode, string(body)) + } + return nil +} + +func (s *TaskService) DeleteTask(id string) error { + body, resp, err := s.Client.Delete(fmt.Sprintf("%s/%s", taskAPIEndpoint, id)) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("could not delete task '%s': HTTP: %d, %s", id, resp.StatusCode, string(body)) + } + + return nil +} diff --git a/nexus3/pkg/task/service_test.go b/nexus3/pkg/task/service_test.go new file mode 100644 index 0000000..43506cd --- /dev/null +++ b/nexus3/pkg/task/service_test.go @@ -0,0 +1,202 @@ +package task + +import ( + "testing" + "time" + + "github.com/datadrivers/go-nexus-client/nexus3/schema/task" + + "github.com/datadrivers/go-nexus-client/nexus3/pkg/client" + "github.com/datadrivers/go-nexus-client/nexus3/pkg/tools" + "github.com/stretchr/testify/assert" +) + +const dummyTask = "dummy" + +var ( + testClient *client.Client = nil +) + +func getTestClient() *client.Client { + if testClient != nil { + return testClient + } + return client.NewClient(getDefaultConfig()) +} + +func getTestService() *TaskService { + return NewTaskService(getTestClient()) +} + +func getDefaultConfig() client.Config { + timeout := tools.GetEnv("NEXUS_TIMEOUT", 30).(int) + return client.Config{ + Insecure: tools.GetEnv("NEXUS_INSECURE_SKIP_VERIFY", true).(bool), + Password: tools.GetEnv("NEXUS_PASSWORD", "admin123").(string), + URL: tools.GetEnv("NEXUS_URL", "http://127.0.0.1:8081").(string), + Username: tools.GetEnv("NEXUS_USERNAME", "admin").(string), + Timeout: &timeout, + } +} + +func TestFreezeAndReleaseTaskState(t *testing.T) { + s := getTestService() + + tasks, _, err := s.ListTasks(nil, nil) + if err != nil { + assert.Failf(t, "fail to list task", err.Error()) + return + } + for _, task := range tasks { + assert.NotEmpty(t, task.ID) + assert.NotEmpty(t, task.Name) + assert.NotEmpty(t, task.Type) + assert.NotEmpty(t, task.CurrentState) + } + + // test get task api + if len(tasks) > 0 { + task, err := s.GetTask(tasks[0].ID) + if err != nil { + assert.Failf(t, "fail to run task", err.Error()) + return + } + assert.NotEmpty(t, task.ID) + assert.NotEmpty(t, task.Name) + assert.NotEmpty(t, task.Type) + assert.NotEmpty(t, task.CurrentState) + } +} + +func TestTaskService_CreateTask(t *testing.T) { + if tools.GetEnv("SKIP_PRO_TESTS", "false") == "true" { + t.Skip("Skipping Nexus Pro tests") + } + s := getTestService() + newTask := getTestTask() + createdTask, err := s.CreateTask(newTask) + if err != nil { + assert.Failf(t, "fail to create task", err.Error()) + return + } + assert.NotNil(t, createdTask) +} + +func getTestTask() *task.TaskCreateStruct { + newTask := &task.TaskCreateStruct{ + Name: "test-task", + Type: "tags.cleanup", + Enabled: true, + AlertEmail: "abc@acb.com", + NotificationCondition: "FAILURE", + Frequency: &task.FrequencyXO{ + Schedule: "manual", + StartDate: int(time.Now().Unix()), + TimeZoneOffset: "-08:00", + CronExpression: "string", + }, + Properties: map[string]interface{}{}, + } + return newTask +} + +func TestTaskService_UpdateTask(t *testing.T) { + if tools.GetEnv("SKIP_PRO_TESTS", "false") == "true" { + t.Skip("Skipping Nexus Pro tests") + } + s := getTestService() + newTask := getTestTask() + createdTask, err := s.CreateTask(newTask) + if err != nil { + assert.Failf(t, "fail to create task", err.Error()) + return + } + assert.NotNil(t, createdTask) + newTask.Type = "" + newTask.Name = "test-task-updated" + err = s.UpdateTask(createdTask.ID, newTask) + if err != nil { + assert.Failf(t, "fail to update task", err.Error()) + return + } + updatedTask, err := s.GetTask(createdTask.ID) + if err != nil { + assert.Failf(t, "fail to get task", err.Error()) + return + } + assert.NotNil(t, updatedTask) + assert.Equal(t, newTask.Name, updatedTask.Name, "newTask.Name should be equal to updatedTask.Name") + + err = s.UpdateTask(dummyTask, newTask) + assert.NotNil(t, err) +} + +func TestTaskService_DeleteTaskTask(t *testing.T) { + if tools.GetEnv("SKIP_PRO_TESTS", "false") == "true" { + t.Skip("Skipping Nexus Pro tests") + } + s := getTestService() + newTask := getTestTask() + createdTask, err := s.CreateTask(newTask) + if err != nil { + assert.Failf(t, "fail to create task", err.Error()) + return + } + assert.NotNil(t, createdTask) + err = s.DeleteTask(createdTask.ID) + removedTask, err := s.GetTask(createdTask.ID) + if err != nil { + assert.Failf(t, "fail to update task", err.Error()) + return + } + assert.Nil(t, removedTask) + err = s.DeleteTask(dummyTask) + assert.NotNil(t, err) +} + +func TestTaskService_RunTask(t *testing.T) { + if tools.GetEnv("SKIP_PRO_TESTS", "false") == "true" { + t.Skip("Skipping Nexus Pro tests") + } + s := getTestService() + newTask := getTestTask() + createdTask, err := s.CreateTask(newTask) + if err != nil { + assert.Failf(t, "fail to create task", err.Error()) + return + } + assert.NotNil(t, createdTask) + err = s.RunTask(createdTask.ID) + if err != nil { + assert.Failf(t, "fail to run task", err.Error()) + return + } + err = s.RunTask(dummyTask) + assert.NotNil(t, err) +} + +func TestTaskService_StopTask(t *testing.T) { + if tools.GetEnv("SKIP_PRO_TESTS", "false") == "true" { + t.Skip("Skipping Nexus Pro tests") + } + s := getTestService() + newTask := getTestTask() + createdTask, err := s.CreateTask(newTask) + if err != nil { + assert.Failf(t, "fail to create task", err.Error()) + return + } + assert.NotNil(t, createdTask) + err = s.RunTask(createdTask.ID) + if err != nil { + assert.Failf(t, "fail to run task", err.Error()) + return + } + err = s.StopTask(createdTask.ID) + if err != nil { + assert.Failf(t, "fail to stop task", err.Error()) + return + } + err = s.StopTask(dummyTask) + assert.NotNil(t, err) +} diff --git a/nexus3/schema/pagination.go b/nexus3/schema/pagination.go new file mode 100644 index 0000000..ed1bdb5 --- /dev/null +++ b/nexus3/schema/pagination.go @@ -0,0 +1,6 @@ +package schema + +type PaginationResult[T any] struct { + Items []T `json:"items"` + ContinuationToken *string `json:"continuationToken"` +} diff --git a/nexus3/schema/task/task.go b/nexus3/schema/task/task.go new file mode 100644 index 0000000..eaf1c69 --- /dev/null +++ b/nexus3/schema/task/task.go @@ -0,0 +1,31 @@ +package task + +type FrequencyXO struct { + Schedule string `json:"schedule"` + StartDate int `json:"startDate,omitempty"` + TimeZoneOffset string `json:"timeZoneOffset,omitempty"` + RecurringDays []interface{} `json:"recurringDays,omitempty"` + CronExpression string `json:"cronExpression,omitempty"` +} + +type Task struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Message string `json:"message,omitempty"` + CurrentState string `json:"currentState,omitempty"` + Frequency *FrequencyXO `json:"frequency,omitempty"` + NextRun string `json:"nextRun,omitempty"` + LastRun string `json:"lastRun,omitempty"` +} + +type TaskCreateStruct struct { + Type string `json:"type,omitempty"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + AlertEmail string `json:"alertEmail,omitempty"` + NotificationCondition string `json:"notificationCondition"` + Frequency *FrequencyXO `json:"frequency,omitempty"` + Message string `json:"message,omitempty"` + Properties map[string]interface{} `json:"properties,omitempty"` +}