Skip to content

Commit deeb062

Browse files
mirkoCrobumirkoCrobulucarin91
authored
Implement a "settings" (key-value) API (#685)
Co-authored-by: mirkoCrobu <mirkocrobu@NB-0531.localdomain> Co-authored-by: lucarin91 <lucarin@protonmail.com>
1 parent c8e287c commit deeb062

File tree

13 files changed

+1283
-3
lines changed

13 files changed

+1283
-3
lines changed

cmd/gendoc/docs.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"golang.org/x/text/language"
1616

1717
"github.com/bcmi-labs/orchestrator/internal/api/handlers"
18+
"github.com/bcmi-labs/orchestrator/internal/api/models"
1819
"github.com/bcmi-labs/orchestrator/internal/orchestrator"
1920
"github.com/bcmi-labs/orchestrator/internal/orchestrator/app"
2021
"github.com/bcmi-labs/orchestrator/internal/orchestrator/bricks"
@@ -28,6 +29,7 @@ const (
2829
BrickTag Tag = "Brick"
2930
AIModelsTag Tag = "AIModels"
3031
SystemTag Tag = "System"
32+
Property Tag = "Property"
3133
)
3234

3335
var validTags = []Tag{ApplicationTag, BrickTag, AIModelsTag, SystemTag}
@@ -187,6 +189,7 @@ func NewOpenApiGenerator(version string) *Generator {
187189
// to manually remove the pkg prefix.
188190
reflector.DefaultOptions = append(reflector.DefaultOptions,
189191
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
192+
190193
if params.Value.Type() == reflect.TypeOf(orchestrator.Status("")) {
191194
params.Schema.WithRef("#/components/schemas/Status")
192195
return true, nil
@@ -249,6 +252,90 @@ type ErrorResponse struct {
249252
func (g *Generator) InitOperations() {
250253

251254
operations := []OperationConfig{
255+
{
256+
OperationId: "DeleteProperty",
257+
Method: http.MethodDelete,
258+
Path: "/v1/properties/{key}",
259+
Request: (*struct {
260+
ID string `path:"key" description:"property key."`
261+
})(nil),
262+
CustomSuccessResponse: &CustomResponseDef{
263+
ContentType: "application/json",
264+
DataStructure: nil,
265+
Description: "Successful response",
266+
StatusCode: http.StatusNoContent,
267+
},
268+
Description: "Delete the property by the provided key.",
269+
Summary: "Delete property by key",
270+
Tags: []Tag{Property},
271+
PossibleErrors: []ErrorResponse{
272+
{StatusCode: http.StatusNotFound, Reference: "#/components/responses/NotFound"},
273+
{StatusCode: http.StatusBadRequest, Reference: "#/components/responses/BadRequest"},
274+
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},
275+
},
276+
},
277+
{
278+
OperationId: "UpdateProperty",
279+
Method: http.MethodPut,
280+
Path: "/v1/properties/{key}",
281+
Parameters: (*struct {
282+
ID string `path:"key" description:"property key."`
283+
})(nil),
284+
Request: []byte{},
285+
CustomSuccessResponse: &CustomResponseDef{
286+
ContentType: "application/octet-stream",
287+
DataStructure: []byte{},
288+
Description: "Successful response",
289+
StatusCode: http.StatusOK,
290+
},
291+
Description: "Update or create a new property.",
292+
Summary: "Upsert property",
293+
Tags: []Tag{Property},
294+
PossibleErrors: []ErrorResponse{
295+
{StatusCode: http.StatusNotFound, Reference: "#/components/responses/NotFound"},
296+
{StatusCode: http.StatusBadRequest, Reference: "#/components/responses/BadRequest"},
297+
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},
298+
},
299+
},
300+
{
301+
OperationId: "GetProperty",
302+
Method: http.MethodGet,
303+
Path: "/v1/properties/{key}",
304+
Parameters: (*struct {
305+
ID string `path:"key" description:"property key."`
306+
})(nil),
307+
CustomSuccessResponse: &CustomResponseDef{
308+
ContentType: "application/octet-stream",
309+
DataStructure: []byte{},
310+
Description: "Successful response",
311+
StatusCode: http.StatusOK,
312+
},
313+
Description: "Return a single property by the provided key.",
314+
Summary: "Get property by key",
315+
Tags: []Tag{Property},
316+
PossibleErrors: []ErrorResponse{
317+
{StatusCode: http.StatusNotFound, Reference: "#/components/responses/NotFound"},
318+
{StatusCode: http.StatusBadRequest, Reference: "#/components/responses/BadRequest"},
319+
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},
320+
},
321+
},
322+
{
323+
OperationId: "GetPropertyKeys",
324+
Method: http.MethodGet,
325+
Path: "/v1/properties",
326+
CustomSuccessResponse: &CustomResponseDef{
327+
ContentType: "application/json",
328+
DataStructure: models.PropertyKeysResponse{},
329+
Description: "Successful response",
330+
StatusCode: http.StatusOK,
331+
},
332+
Description: "Return the list of system properties.",
333+
Summary: "Get system properties",
334+
Tags: []Tag{Property},
335+
PossibleErrors: []ErrorResponse{
336+
{StatusCode: http.StatusInternalServerError, Reference: "#/components/responses/InternalServerError"},
337+
},
338+
},
252339
{
253340
OperationId: "getAppPorts",
254341
Method: http.MethodGet,

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ require (
3030
github.com/fatih/color v1.18.0
3131
github.com/fsnotify/fsnotify v1.9.0
3232
github.com/goccy/go-yaml v1.18.0
33+
github.com/gofrs/flock v0.12.1
3334
github.com/google/go-cmp v0.7.0
3435
github.com/google/renameio/v2 v2.0.0
3536
github.com/gorilla/websocket v1.5.0
@@ -131,7 +132,6 @@ require (
131132
github.com/go-openapi/jsonreference v0.20.2 // indirect
132133
github.com/go-openapi/swag v0.23.0 // indirect
133134
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
134-
github.com/gofrs/flock v0.12.1 // indirect
135135
github.com/gofrs/uuid/v5 v5.3.2 // indirect
136136
github.com/gogo/protobuf v1.3.2 // indirect
137137
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect

internal/api/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ func NewHTTPRouter(
4343
mux.Handle("GET /v1/bricks", handlers.HandleBrickList(brickService))
4444
mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService))
4545

46+
mux.Handle("GET /v1/properties", handlers.HandlePropertyKeys(cfg))
47+
mux.Handle("GET /v1/properties/{key}", handlers.HandlePropertyGet(cfg))
48+
mux.Handle("PUT /v1/properties/{key}", handlers.HandlePropertyUpsert(cfg))
49+
mux.Handle("DELETE /v1/properties/{key}", handlers.HandlePropertyDelete(cfg))
50+
4651
mux.Handle("GET /v1/system/update/check", handlers.HandleCheckUpgradable(updater))
4752
mux.Handle("GET /v1/system/update/events", handlers.HandleUpdateEvents(updater))
4853
mux.Handle("PUT /v1/system/update/apply", handlers.HandleUpdateApply(updater))

internal/api/docs/openapi.yaml

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,112 @@ paths:
651651
summary: Get AI model details
652652
tags:
653653
- AIModels
654+
/v1/properties:
655+
get:
656+
description: Return the list of system properties.
657+
operationId: GetPropertyKeys
658+
responses:
659+
"200":
660+
content:
661+
application/json:
662+
schema:
663+
$ref: '#/components/schemas/PropertyKeysResponse'
664+
description: Successful response
665+
"500":
666+
$ref: '#/components/responses/InternalServerError'
667+
summary: Get system properties
668+
tags:
669+
- Property
670+
/v1/properties/{key}:
671+
delete:
672+
description: Delete the property by the provided key.
673+
operationId: DeleteProperty
674+
parameters:
675+
- description: property key.
676+
in: path
677+
name: key
678+
required: true
679+
schema:
680+
description: property key.
681+
type: string
682+
responses:
683+
"204":
684+
content:
685+
application/json:
686+
schema:
687+
type: string
688+
description: Successful response
689+
"400":
690+
$ref: '#/components/responses/BadRequest'
691+
"404":
692+
$ref: '#/components/responses/NotFound'
693+
"500":
694+
$ref: '#/components/responses/InternalServerError'
695+
summary: Delete property by key
696+
tags:
697+
- Property
698+
get:
699+
description: Return a single property by the provided key.
700+
operationId: GetProperty
701+
parameters:
702+
- description: property key.
703+
in: path
704+
name: key
705+
required: true
706+
schema:
707+
description: property key.
708+
type: string
709+
responses:
710+
"200":
711+
content:
712+
application/octet-stream:
713+
schema:
714+
format: base64
715+
type: string
716+
description: Successful response
717+
"400":
718+
$ref: '#/components/responses/BadRequest'
719+
"404":
720+
$ref: '#/components/responses/NotFound'
721+
"500":
722+
$ref: '#/components/responses/InternalServerError'
723+
summary: Get property by key
724+
tags:
725+
- Property
726+
put:
727+
description: Update or create a new property.
728+
operationId: UpdateProperty
729+
parameters:
730+
- description: property key.
731+
in: path
732+
name: key
733+
required: true
734+
schema:
735+
description: property key.
736+
type: string
737+
requestBody:
738+
content:
739+
application/json:
740+
schema:
741+
format: base64
742+
type: string
743+
responses:
744+
"200":
745+
content:
746+
application/octet-stream:
747+
schema:
748+
format: base64
749+
type: string
750+
description: Successful response
751+
"400":
752+
$ref: '#/components/responses/BadRequest'
753+
"404":
754+
$ref: '#/components/responses/NotFound'
755+
"500":
756+
$ref: '#/components/responses/InternalServerError'
757+
summary: Upsert property
758+
tags:
759+
- Property
654760
/v1/system/resources:
655761
get:
656762
description: Returns the system resources usage, such as memory, disk and CPU.
@@ -1172,6 +1278,14 @@ components:
11721278
example: brick:data-storage
11731279
type: string
11741280
type: object
1281+
PropertyKeysResponse:
1282+
properties:
1283+
keys:
1284+
items:
1285+
type: string
1286+
nullable: true
1287+
type: array
1288+
type: object
11751289
Status:
11761290
description: Application status
11771291
enum:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package handlers
2+
3+
import (
4+
"errors"
5+
"io"
6+
"log/slog"
7+
"net/http"
8+
9+
"github.com/bcmi-labs/orchestrator/internal/api/models"
10+
"github.com/bcmi-labs/orchestrator/internal/orchestrator/config"
11+
properties "github.com/bcmi-labs/orchestrator/internal/orchestrator/system_properties"
12+
"github.com/bcmi-labs/orchestrator/pkg/render"
13+
)
14+
15+
func HandlePropertyKeys(cfg config.Configuration) http.HandlerFunc {
16+
return func(w http.ResponseWriter, r *http.Request) {
17+
propertyList, err := properties.ReadPropertyKeys(cfg.DataDir().Join("properties.msgpack").String())
18+
if err != nil {
19+
slog.Error("Unable to retrieve list", slog.String("error", err.Error()))
20+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to find the list"})
21+
return
22+
}
23+
render.EncodeResponse(w, http.StatusOK, models.PropertyKeysResponse{Keys: propertyList})
24+
}
25+
}
26+
27+
func HandlePropertyGet(cfg config.Configuration) http.HandlerFunc {
28+
return func(w http.ResponseWriter, r *http.Request) {
29+
key := r.PathValue("key")
30+
31+
property, found, err := properties.GetProperty(cfg.DataDir().Join("properties.msgpack").String(), key)
32+
if err != nil {
33+
if errors.Is(err, properties.ErrInvalidKey) {
34+
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: err.Error()})
35+
return
36+
}
37+
slog.Error("Unable to retrieve property", "key", key, "error", err.Error())
38+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "Unable to retrieve property"})
39+
return
40+
}
41+
42+
if !found {
43+
render.EncodeResponse(w, http.StatusNotFound, nil)
44+
return
45+
}
46+
47+
render.EncodeByteResponse(w, http.StatusOK, property)
48+
}
49+
}
50+
51+
func HandlePropertyUpsert(cfg config.Configuration) http.HandlerFunc {
52+
return func(w http.ResponseWriter, r *http.Request) {
53+
key := r.PathValue("key")
54+
55+
reqBody, err := io.ReadAll(r.Body)
56+
if err != nil {
57+
slog.Warn("Failed to read request body", "error", err.Error())
58+
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "invalid body"})
59+
return
60+
}
61+
defer r.Body.Close()
62+
if len(reqBody) == 0 {
63+
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "body cannot be empty"})
64+
return
65+
}
66+
67+
err = properties.UpsertProperty(cfg.DataDir().Join("properties.msgpack").String(), key, reqBody)
68+
if err != nil {
69+
if errors.Is(err, properties.ErrInvalidKey) {
70+
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: err.Error()})
71+
return
72+
}
73+
slog.Error("Failed to upsert property", "key", key, "error", err.Error())
74+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "failed to update property"})
75+
return
76+
}
77+
render.EncodeByteResponse(w, http.StatusOK, reqBody)
78+
}
79+
}
80+
81+
func HandlePropertyDelete(cfg config.Configuration) http.HandlerFunc {
82+
return func(w http.ResponseWriter, r *http.Request) {
83+
key := r.PathValue("key")
84+
found, err := properties.DeleteProperty(cfg.DataDir().Join("properties.msgpack").String(), key)
85+
if err != nil {
86+
if errors.Is(err, properties.ErrInvalidKey) {
87+
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: err.Error()})
88+
return
89+
}
90+
slog.Error("Failed to delete property", "key", key, "error", err.Error())
91+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "failed to delete property"})
92+
return
93+
}
94+
if !found {
95+
render.EncodeResponse(w, http.StatusNotFound, nil)
96+
return
97+
}
98+
render.EncodeResponse(w, http.StatusNoContent, nil)
99+
}
100+
}

internal/api/models/properties.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package models
2+
3+
type PropertyKeysResponse struct {
4+
Keys []string `json:"keys"`
5+
}

0 commit comments

Comments
 (0)