diff --git a/nexus3/nexus.go b/nexus3/nexus.go index 2677d994..2bec6dac 100644 --- a/nexus3/nexus.go +++ b/nexus3/nexus.go @@ -7,6 +7,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 ( @@ -27,6 +28,7 @@ type NexusClient struct { CleanupPolicy *cleanup.CleanupPolicyService Security *security.SecurityService Script *ScriptService + Task *task.TaskService ReadOnly *readonly.ReadOnlyService MailConfig *MailConfigService } @@ -42,6 +44,7 @@ func NewClient(config client.Config) *NexusClient { CleanupPolicy: cleanup.NewCleanupPolicyService(client), Security: security.NewSecurityService(client), Script: NewScriptService(client), + Task: task.NewTaskService(client), ReadOnly: readonly.NewReadOnlyService(client), MailConfig: NewMailConfigService(client), } diff --git a/nexus3/pkg/cleanup/service.go b/nexus3/pkg/cleanup/service.go index e2f009d8..e84d30ec 100644 --- a/nexus3/pkg/cleanup/service.go +++ b/nexus3/pkg/cleanup/service.go @@ -3,11 +3,10 @@ package cleanup import ( "encoding/json" "fmt" - "net/http" - "github.com/datadrivers/go-nexus-client/nexus3/pkg/client" "github.com/datadrivers/go-nexus-client/nexus3/pkg/tools" "github.com/datadrivers/go-nexus-client/nexus3/schema/cleanuppolicies" + "net/http" ) const ( @@ -20,21 +19,24 @@ func NewCleanupPolicyService(c *client.Client) *CleanupPolicyService { return &CleanupPolicyService{Client: c} } -func (s *CleanupPolicyService) Create(policy *cleanuppolicies.CleanupPolicy) error { +func (s *CleanupPolicyService) Create(policy *cleanuppolicies.CleanupPolicy) (*cleanuppolicies.CleanupPolicy, error) { data, err := tools.JsonMarshalInterfaceToIOReader(policy) if err != nil { - return err + return nil, err } body, resp, err := s.Client.Post(cleanupAPIEndpoint, data) if err != nil { - return err + return nil, err } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { - return fmt.Errorf("could not create cleanup policy '%s': HTTP: %d, %s", policy.Name, resp.StatusCode, string(body)) + return nil, fmt.Errorf("could not create cleanup policy '%s': HTTP: %d, %s", policy.Name, resp.StatusCode, string(body)) } - return nil + createdPolicy := &cleanuppolicies.CleanupPolicy{} + if err := json.Unmarshal(body, createdPolicy); err != nil { + return nil, fmt.Errorf("could not unmarshal created cleanup policy: %v", err) + } + return createdPolicy, nil } - func (s *CleanupPolicyService) Get(name string) (*cleanuppolicies.CleanupPolicy, error) { body, resp, err := s.Client.Get(fmt.Sprintf("%s/%s", cleanupAPIEndpoint, name), nil) @@ -46,7 +48,7 @@ func (s *CleanupPolicyService) Get(name string) (*cleanuppolicies.CleanupPolicy, } policy := &cleanuppolicies.CleanupPolicy{} if err := json.Unmarshal(body, policy); err != nil { - return nil, fmt.Errorf("could not unmarshal repository: %v", err) + return nil, fmt.Errorf("could not unmarshal CleanupPolicy: %v", err) } return policy, nil diff --git a/nexus3/pkg/task/service.go b/nexus3/pkg/task/service.go new file mode 100644 index 00000000..6782f546 --- /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 00000000..2efb2d84 --- /dev/null +++ b/nexus3/pkg/task/service_test.go @@ -0,0 +1,201 @@ +package task + +import ( + "github.com/datadrivers/go-nexus-client/nexus3/schema/task" + "testing" + "time" + + "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/cleanuppolicies/cleanup-policies.go b/nexus3/schema/cleanuppolicies/cleanup-policies.go index afb097e7..f9e5aa94 100644 --- a/nexus3/schema/cleanuppolicies/cleanup-policies.go +++ b/nexus3/schema/cleanuppolicies/cleanup-policies.go @@ -35,12 +35,12 @@ const ( ) type CleanupPolicy struct { - Notes *string `json:"notes,omitempty"` // any details on the specific cleanup policy. - CriteriaLastBlobUpdated *int `json:"criteriaLastBlobUpdated,omitempty"` // the age of the component in days. - CriteriaLastDownloaded *int `json:"criteriaLastDownloaded,omitempty"` // the last time the component had been downloaded in days. - CriteriaReleaseType *CriteriaReleaseType `json:"criteriaReleaseType,omitempty"` // is one of: RELEASES_AND_PRERELEASES, PRERELEASES, RELEASES], Only maven2, npm and yum repositories support this field. - CriteriaAssetRegex *string `json:"criteriaAssetRegex,omitempty"` // a regex string to filter for specific asset paths. Not for gitlfs or * - Retain int `json:"retain,omitempty"` // number of versions to keep. Only available for Docker and Maven release repositories on PostgreSQL deployments. - Name string `json:"name"` // the name of the policy needs to be unique and cannot be edited once set. Only letters, digits, underscores(_), hyphens(-), and dots(.) are allowed and may not start with underscore or dot. - Format RepositoryFormat `json:"format"` // one of ["*", "apt", "bower", "cocoapods", "conan", "conda", "docker", "gitlfs", "go", "helm", "maven2", "npm", "nuget", "p2", "pypi", "r", "raw", "rubygems", "yum"] + Notes string `json:"notes,omitempty"` // any details on the specific cleanup policy. + CriteriaLastBlobUpdated *int `json:"criteriaLastBlobUpdated,omitempty"` // the age of the component in days. + CriteriaLastDownloaded *int `json:"criteriaLastDownloaded,omitempty"` // the last time the component had been downloaded in days. + CriteriaReleaseType CriteriaReleaseType `json:"criteriaReleaseType,omitempty"` // is one of: RELEASES_AND_PRERELEASES, PRERELEASES, RELEASES], Only maven2, npm and yum repositories support this field. + CriteriaAssetRegex string `json:"criteriaAssetRegex,omitempty"` // a regex string to filter for specific asset paths. Not for gitlfs or * + Retain *int `json:"retain,omitempty"` // number of versions to keep. Only available for Docker and Maven release repositories on PostgreSQL deployments. + Name string `json:"name"` // the name of the policy needs to be unique and cannot be edited once set. Only letters, digits, underscores(_), hyphens(-), and dots(.) are allowed and may not start with underscore or dot. + Format RepositoryFormat `json:"format"` // one of ["*", "apt", "bower", "cocoapods", "conan", "conda", "docker", "gitlfs", "go", "helm", "maven2", "npm", "nuget", "p2", "pypi", "r", "raw", "rubygems", "yum"] } diff --git a/nexus3/schema/pagination.go b/nexus3/schema/pagination.go new file mode 100644 index 00000000..ed1bdb50 --- /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 00000000..05ddc738 --- /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"` +}