Skip to content

Commit 7864e35

Browse files
committed
feat(image): add task endpoints
1 parent 76903af commit 7864e35

11 files changed

Lines changed: 297 additions & 0 deletions

File tree

internal/api/image/router.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func NewHandler(cfg config.Config) Handler {
3434
storeimage.NewMemoryRepository(),
3535
storeimage.NewMemoryDataRepository(),
3636
storeimage.NewMemoryMemberRepository(),
37+
storeimage.NewMemoryTaskRepository(),
3738
clock.Wall(),
3839
idgen.Random(),
3940
),
@@ -63,6 +64,9 @@ func (h Handler) Router() http.Handler {
6364
router.Put("/images/{image_id}/stage", h.stageImageData)
6465
router.Post("/images/{image_id}/import", h.importImageData)
6566
router.Get("/info/import", h.getImportInfo)
67+
router.Get("/tasks", h.listTasks)
68+
router.Post("/tasks", h.createTask)
69+
router.Get("/tasks/{task_id}", h.getTask)
6670
router.Get("/images/{image_id}/members", h.listMembers)
6771
router.Post("/images/{image_id}/members", h.createMember)
6872
router.Get("/images/{image_id}/members/{member_id}", h.getMember)

internal/api/image/task.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package image
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
8+
"github.com/JSYoo5B/SandStack/internal/api/respond"
9+
appimage "github.com/JSYoo5B/SandStack/internal/app/image"
10+
"github.com/go-chi/chi/v5"
11+
)
12+
13+
func (h Handler) listTasks(w http.ResponseWriter, r *http.Request) {
14+
respond.JSON(w, http.StatusOK, taskListResponse{
15+
Tasks: toTaskDocuments(h.service.ListTasks()),
16+
Next: "",
17+
})
18+
}
19+
20+
func (h Handler) createTask(w http.ResponseWriter, r *http.Request) {
21+
var request createTaskRequest
22+
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
23+
respond.Error(w, http.StatusBadRequest, "invalid JSON request body")
24+
return
25+
}
26+
27+
task := h.service.CreateTask(request.createTask())
28+
29+
respond.JSON(w, http.StatusCreated, toTaskDocument(task))
30+
}
31+
32+
func (h Handler) getTask(w http.ResponseWriter, r *http.Request) {
33+
task, err := h.service.GetTask(chi.URLParam(r, "task_id"))
34+
if errors.Is(err, appimage.ErrTaskNotFound) {
35+
respond.Error(w, http.StatusNotFound, "task not found")
36+
return
37+
}
38+
if err != nil {
39+
respond.Error(w, http.StatusInternalServerError, "task lookup failed")
40+
return
41+
}
42+
43+
respond.JSON(w, http.StatusOK, toTaskDocument(task))
44+
}

internal/api/image/task_dto.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package image
2+
3+
import appimage "github.com/JSYoo5B/SandStack/internal/app/image"
4+
5+
type createTaskRequest struct {
6+
Type string `json:"type"`
7+
Input map[string]any `json:"input"`
8+
}
9+
10+
type taskListResponse struct {
11+
Tasks []taskDocument `json:"tasks"`
12+
Next string `json:"next"`
13+
}
14+
15+
type taskDocument struct {
16+
ID string `json:"id"`
17+
Type string `json:"type"`
18+
Status string `json:"status"`
19+
Input map[string]any `json:"input"`
20+
Result map[string]any `json:"result"`
21+
Owner string `json:"owner"`
22+
Message string `json:"message"`
23+
ExpiresAt string `json:"expires_at"`
24+
CreatedAt string `json:"created_at"`
25+
UpdatedAt string `json:"updated_at"`
26+
Self string `json:"self"`
27+
Schema string `json:"schema"`
28+
}
29+
30+
func (r createTaskRequest) createTask() appimage.CreateTask {
31+
return appimage.CreateTask{
32+
Type: r.Type,
33+
Input: r.Input,
34+
}
35+
}
36+
37+
func toTaskDocuments(tasks []appimage.Task) []taskDocument {
38+
documents := make([]taskDocument, 0, len(tasks))
39+
for _, task := range tasks {
40+
documents = append(documents, toTaskDocument(task))
41+
}
42+
43+
return documents
44+
}
45+
46+
func toTaskDocument(task appimage.Task) taskDocument {
47+
return taskDocument{
48+
ID: task.ID,
49+
Type: task.Type,
50+
Status: task.Status,
51+
Input: task.Input,
52+
Result: task.Result,
53+
Owner: task.Owner,
54+
Message: task.Message,
55+
ExpiresAt: task.ExpiresAt,
56+
CreatedAt: task.CreatedAt,
57+
UpdatedAt: task.UpdatedAt,
58+
Self: "/v2/tasks/" + task.ID,
59+
Schema: "/v2/schemas/task",
60+
}
61+
}

internal/api/image/task_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package image_test
2+
3+
import (
4+
"context"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/JSYoo5B/SandStack/internal/api/image"
9+
"github.com/JSYoo5B/SandStack/internal/testhelper"
10+
"github.com/gophercloud/gophercloud/v2/openstack/image/v2/tasks"
11+
"github.com/stretchr/testify/suite"
12+
)
13+
14+
type TaskSuite struct {
15+
suite.Suite
16+
server *httptest.Server
17+
}
18+
19+
func TestTaskSuite(t *testing.T) {
20+
suite.Run(t, new(TaskSuite))
21+
}
22+
23+
func (s *TaskSuite) SetupTest() {
24+
s.server = httptest.NewServer(
25+
image.NewRouter(testhelper.DefaultConfig()),
26+
)
27+
}
28+
29+
func (s *TaskSuite) TearDownTest() {
30+
s.server.Close()
31+
}
32+
33+
func (s *TaskSuite) TestCreateGetAndListTask() {
34+
client := testhelper.ServiceClient(s.server.URL)
35+
36+
created, err := tasks.Create(
37+
context.Background(),
38+
client,
39+
tasks.CreateOpts{
40+
Type: "import",
41+
Input: map[string]any{
42+
"image_id": "image-1",
43+
},
44+
},
45+
).Extract()
46+
s.Require().NoError(err)
47+
48+
found, err := tasks.Get(context.Background(), client, created.ID).Extract()
49+
s.Require().NoError(err)
50+
51+
pages, err := tasks.List(client, nil).AllPages(context.Background())
52+
s.Require().NoError(err)
53+
54+
listed, err := tasks.ExtractTasks(pages)
55+
s.Require().NoError(err)
56+
57+
s.Assert().Equal("import", found.Type)
58+
s.Assert().Equal(tasks.TaskStatusPending, tasks.TaskStatus(found.Status))
59+
s.Require().Len(listed, 1)
60+
s.Assert().Equal(created.ID, listed[0].ID)
61+
}

internal/api/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func NewRouter(cfg config.Config) http.Handler {
5656
storeimage.NewMemoryRepository(),
5757
storeimage.NewMemoryDataRepository(),
5858
storeimage.NewMemoryMemberRepository(),
59+
storeimage.NewMemoryTaskRepository(),
5960
clock.Wall(),
6061
idgen.Random(),
6162
)

internal/app/image/repository.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ type MemberRepository interface {
2424
Delete(imageID string, memberID string) error
2525
Reset()
2626
}
27+
28+
type TaskRepository interface {
29+
Create(task Task) Task
30+
List() []Task
31+
Get(id string) (Task, error)
32+
Reset()
33+
}

internal/app/image/service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
)
1010

1111
var ErrImageNotFound = errors.New("image not found")
12+
var ErrTaskNotFound = errors.New("task not found")
1213

1314
type Service struct {
1415
repository Repository
1516
dataRepository DataRepository
1617
memberRepository MemberRepository
18+
taskRepository TaskRepository
1719
clock clock.Clock
1820
idGen idgen.Generator
1921
}
@@ -22,13 +24,15 @@ func NewServiceWithRuntime(
2224
repository Repository,
2325
dataRepository DataRepository,
2426
memberRepository MemberRepository,
27+
taskRepository TaskRepository,
2528
clock clock.Clock,
2629
idGen idgen.Generator,
2730
) *Service {
2831
return &Service{
2932
repository: repository,
3033
dataRepository: dataRepository,
3134
memberRepository: memberRepository,
35+
taskRepository: taskRepository,
3236
clock: clock,
3337
idGen: idGen,
3438
}
@@ -74,4 +78,5 @@ func (s *Service) Reset() {
7478
s.repository.Reset()
7579
s.dataRepository.Reset()
7680
s.memberRepository.Reset()
81+
s.taskRepository.Reset()
7782
}

internal/app/image/service_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func (s *ServiceSuite) TestCreateImageUsesInjectedClock() {
2525
storeimage.NewMemoryRepository(),
2626
storeimage.NewMemoryDataRepository(),
2727
storeimage.NewMemoryMemberRepository(),
28+
storeimage.NewMemoryTaskRepository(),
2829
clock.Fixed(now),
2930
idgen.Random(),
3031
)
@@ -44,6 +45,7 @@ func (s *ServiceSuite) TestCreateImageUsesInjectedIDGenerator() {
4445
storeimage.NewMemoryRepository(),
4546
storeimage.NewMemoryDataRepository(),
4647
storeimage.NewMemoryMemberRepository(),
48+
storeimage.NewMemoryTaskRepository(),
4749
clock.Fixed(time.Time{}),
4850
idgen.Fixed("image-id"),
4951
)
@@ -62,6 +64,7 @@ func (s *ServiceSuite) TestResetClearsImages() {
6264
storeimage.NewMemoryRepository(),
6365
storeimage.NewMemoryDataRepository(),
6466
storeimage.NewMemoryMemberRepository(),
67+
storeimage.NewMemoryTaskRepository(),
6568
clock.Fixed(time.Time{}),
6669
idgen.Fixed("image-id"),
6770
)
@@ -74,6 +77,7 @@ func (s *ServiceSuite) TestResetClearsImages() {
7477
s.Require().NoError(err)
7578
_, err = service.CreateMember(created.ID, "project-1")
7679
s.Require().NoError(err)
80+
task := service.CreateTask(image.CreateTask{Type: "import"})
7781

7882
service.Reset()
7983

@@ -83,4 +87,6 @@ func (s *ServiceSuite) TestResetClearsImages() {
8387
members, err := service.ListMembers(created.ID)
8488
s.ErrorIs(err, image.ErrImageNotFound)
8589
s.Nil(members)
90+
_, err = service.GetTask(task.ID)
91+
s.ErrorIs(err, image.ErrTaskNotFound)
8692
}

internal/app/image/task.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package image
2+
3+
import "time"
4+
5+
func (s *Service) CreateTask(input CreateTask) Task {
6+
now := s.clock.Now().UTC()
7+
task := Task{
8+
ID: "task-" + s.idGen.Hex(16),
9+
Type: input.Type,
10+
Status: "pending",
11+
Input: input.Input,
12+
Result: map[string]any{},
13+
Owner: "admin",
14+
ExpiresAt: now.Add(24 * time.Hour).Format(time.RFC3339),
15+
CreatedAt: now.Format(time.RFC3339),
16+
UpdatedAt: now.Format(time.RFC3339),
17+
}
18+
19+
return s.taskRepository.Create(task)
20+
}
21+
22+
func (s *Service) ListTasks() []Task {
23+
return s.taskRepository.List()
24+
}
25+
26+
func (s *Service) GetTask(id string) (Task, error) {
27+
return s.taskRepository.Get(id)
28+
}

internal/app/image/types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,21 @@ type Member struct {
3232
CreatedAt string
3333
UpdatedAt string
3434
}
35+
36+
type Task struct {
37+
ID string
38+
Type string
39+
Status string
40+
Input map[string]any
41+
Result map[string]any
42+
Owner string
43+
Message string
44+
ExpiresAt string
45+
CreatedAt string
46+
UpdatedAt string
47+
}
48+
49+
type CreateTask struct {
50+
Type string
51+
Input map[string]any
52+
}

0 commit comments

Comments
 (0)