Skip to content

Commit b439a2f

Browse files
giulio93lucarin91Your Namedido18
authored
Pull docker containers events for each App
Co-authored-by: lucarin91 <lucarin@protonmail.com> Co-authored-by: Your Name <youremail@example.com> Co-authored-by: Davide <davideneri18@gmail.com>
1 parent de7ae80 commit b439a2f

File tree

8 files changed

+323
-48
lines changed

8 files changed

+323
-48
lines changed

cmd/gendoc/docs.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,33 @@ Contains a JSON object with the details of an error.
520520
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},
521521
},
522522
},
523+
{
524+
OperationId: "getAppsEvents",
525+
Method: http.MethodGet,
526+
Path: "/v1/apps/events",
527+
CustomSuccessResponse: &CustomResponseDef{
528+
ContentType: "text/event-stream",
529+
DataStructure: orchestrator.LogMessage{},
530+
},
531+
Description: `A stream of Server-Sent Events (SSE) that notifies the apps status.
532+
The client will receive events formatted as follows:
533+
534+
**Event 'app'**:
535+
Contains a JSON object with an informational message.
536+
'event: app'
537+
'data: {"id":"dXNlcjpleGFtcG","name":"example-app-for-status-events","description":"My app description","icon":"💻","status":"running","example":false,"default":false}'
538+
539+
**Event 'error'**:
540+
Contains a JSON object with the details of an error.
541+
'event: error'
542+
'data: {"code":"INTERNAL_SERVER_ERROR","message":"An error occurred during operation"}'
543+
`,
544+
Summary: "Get application events",
545+
Tags: []Tag{ApplicationTag},
546+
PossibleErrors: []ErrorResponse{
547+
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},
548+
},
549+
},
523550
{
524551
OperationId: "getAppEvents",
525552
Method: http.MethodGet,
@@ -532,7 +559,7 @@ Contains a JSON object with the details of an error.
532559
DataStructure: orchestrator.LogMessage{},
533560
},
534561
Description: "Returns events for a specific app ",
535-
Summary: "Get application events ",
562+
Summary: "Get application events",
536563
Tags: []Tag{ApplicationTag},
537564
PossibleErrors: []ErrorResponse{
538565
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},

internal/api/docs/openapi.yaml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ paths:
409409
description: OK
410410
"500":
411411
$ref: '#/components/responses/InternalServerError'
412-
summary: 'Get application events '
412+
summary: Get application events
413413
tags:
414414
- Application
415415
/v1/apps/{id}/logs:
@@ -541,6 +541,28 @@ paths:
541541
summary: Stop an existing app/example
542542
tags:
543543
- Application
544+
/v1/apps/events:
545+
get:
546+
description: "A stream of Server-Sent Events (SSE) that notifies the apps status.\nThe
547+
client will receive events formatted as follows:\n\n**Event 'app'**:\nContains
548+
a JSON object with an informational message.\n'event: app'\n'data: {\"id\":\"dXNlcjpleGFtcG\",\"name\":\"example-app-for-status-events\",\"description\":\"My
549+
app description\",\"icon\":\"\U0001F4BB\",\"status\":\"running\",\"example\":false,\"default\":false}'\n\n**Event
550+
'error'**:\nContains a JSON object with the details of an error.\n'event:
551+
error'\n'data: {\"code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"An error
552+
occurred during operation\"}'\n"
553+
operationId: getAppsEvents
554+
responses:
555+
"200":
556+
content:
557+
text/event-stream:
558+
schema:
559+
type: string
560+
description: OK
561+
"500":
562+
$ref: '#/components/responses/InternalServerError'
563+
summary: Get application events
564+
tags:
565+
- Application
544566
/v1/bricks:
545567
get:
546568
description: Returns all the existing bricks. Bricks that are ready to use are

internal/api/handlers/app_status.go

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package handlers
33
import (
44
"log/slog"
55
"net/http"
6-
"time"
76

87
"github.com/docker/cli/cli/command"
98

@@ -28,30 +27,12 @@ func HandlerAppStatus(
2827
}
2928
defer sseStream.Close()
3029

31-
// TODO: maybe we should limit the size of this cache?
32-
stateCache := make(map[string]orchestrator.Status)
33-
34-
for {
35-
apps, err := orchestrator.AppStatus(r.Context(), cfg, dockerCli.Client(), idProvider)
30+
for appStatus, err := range orchestrator.AppStatusEvents(r.Context(), cfg, dockerCli, idProvider) {
3631
if err != nil {
37-
slog.Error("Unable to get apps status", slog.String("error", err.Error()))
38-
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to get apps status"})
39-
return
40-
}
41-
for _, a := range apps {
42-
status, exist := stateCache[a.ID.String()]
43-
if !exist || status != a.Status {
44-
sseStream.Send(render.SSEEvent{Type: "app", Data: a})
45-
}
46-
47-
stateCache[a.ID.String()] = a.Status
48-
}
49-
50-
select {
51-
case <-r.Context().Done():
52-
return
53-
case <-time.After(1 * time.Second):
32+
sseStream.SendError(render.SSEErrorData{Code: render.InternalServiceErr, Message: err.Error()})
33+
continue
5434
}
35+
sseStream.Send(render.SSEEvent{Type: "app", Data: appStatus})
5536
}
5637
}
5738
}

internal/e2e/client/client.gen.go

Lines changed: 102 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/e2e/daemon/app_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package daemon
22

33
import (
4+
"bufio"
45
"context"
56
"encoding/json"
7+
"fmt"
68
"io"
9+
"log"
710
"net/http"
811
"strconv"
912
"strings"
@@ -852,6 +855,66 @@ func TestAppPorts(t *testing.T) {
852855
})
853856
}
854857

858+
func TestGetAppsStatusEvents(t *testing.T) {
859+
860+
httpClient := GetHttpclient(t)
861+
appName := "example-app-for-status-events"
862+
863+
t.Run("StreamAppEvents_Success", func(t *testing.T) {
864+
eventsResp, err := httpClient.GetAppsEvents(t.Context())
865+
require.NoError(t, err)
866+
require.Equal(t, http.StatusOK, eventsResp.StatusCode)
867+
defer eventsResp.Body.Close()
868+
scanner := bufio.NewScanner(eventsResp.Body)
869+
go func() {
870+
createResp, err := httpClient.CreateAppWithResponse(
871+
t.Context(),
872+
&client.CreateAppParams{SkipSketch: f.Ptr(true)},
873+
client.CreateAppRequest{
874+
Icon: f.Ptr("💻"),
875+
Name: appName,
876+
Description: f.Ptr("My app description"),
877+
},
878+
)
879+
require.NoError(t, err)
880+
require.Equal(t, http.StatusCreated, createResp.StatusCode())
881+
appResponse, err := httpClient.StartAppWithResponse(t.Context(), *createResp.JSON201.Id)
882+
require.NoError(t, err)
883+
require.Equal(t, http.StatusOK, appResponse.StatusCode())
884+
}()
885+
var lastStatuses []string
886+
for scanner.Scan() {
887+
line := scanner.Text()
888+
if line == "" {
889+
continue
890+
}
891+
eventData := strings.TrimPrefix(line, "data: ")
892+
if strings.Contains(eventData, `"name":"`+appName+`"`) {
893+
var event map[string]interface{}
894+
err := json.Unmarshal([]byte(eventData), &event)
895+
require.NoError(t, err)
896+
status, ok := event["status"].(string)
897+
require.True(t, ok, "status field missing or not string")
898+
lastStatuses = append(lastStatuses, status)
899+
if len(lastStatuses) > 3 {
900+
lastStatuses = lastStatuses[1:]
901+
}
902+
if len(lastStatuses) == 3 &&
903+
lastStatuses[0] == "stopped" &&
904+
lastStatuses[1] == "running" &&
905+
lastStatuses[2] == "stopped" {
906+
fmt.Println("Desired sequence received, terminating test")
907+
return
908+
}
909+
}
910+
}
911+
if err := scanner.Err(); err != nil {
912+
log.Fatal(fmt.Errorf("error reading event stream: %w", err))
913+
}
914+
915+
})
916+
}
917+
855918
func TestAppList(t *testing.T) {
856919
httpClient := GetHttpclient(t)
857920
t.Run("AppListEmpty_success", func(t *testing.T) {
@@ -880,7 +943,6 @@ func TestAppList(t *testing.T) {
880943
require.Equal(t, http.StatusOK, resp.StatusCode())
881944
require.NotNil(t, resp.JSON200)
882945
require.Equal(t, len(*resp.JSON200.Apps), expectedAppNumber, "The apps list should contain "+strconv.Itoa(expectedAppNumber)+" elements")
883-
884946
})
885947

886948
t.Run("AppListDefault_success", func(t *testing.T) {

internal/e2e/daemon/const.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@ const (
88
noExistingApp = "dXNlcjp0ZXN0LWFwcAw"
99
malformedAppId = "this-is-definitely-not-base64"
1010
noExisitingExample = "ZXhhbXBsZXM6anVzdGJsaW5f"
11-
existingExampleAppId = "ZXhhbXBsZXM6YWlyLXF1YWxpdHktbW9uaXRvcmluZw"
1211
)

0 commit comments

Comments
 (0)