From e7b9d39a7cfdd8704813434c425b560368443c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 30 Sep 2023 17:26:44 +0200 Subject: [PATCH] add labels filter support to API. --- OpenApi.yml | 15 +++++++++++++ client/src/api/api.ts | 36 ++++++++++++++++++++++++------ client/src/components/RoomInfo.vue | 29 +++++++++++++++++------- docs/lables.md | 2 +- internal/api/rooms.go | 16 ++++++++++++- internal/room/containers.go | 35 ++++++++--------------------- internal/room/labels.go | 25 +++++++++++++++++++++ internal/room/manager.go | 13 ++++++----- internal/room/ports.go | 2 +- internal/types/room.go | 22 +++++++++--------- 10 files changed, 136 insertions(+), 59 deletions(-) diff --git a/OpenApi.yml b/OpenApi.yml index 98c9536..e024f23 100644 --- a/OpenApi.yml +++ b/OpenApi.yml @@ -33,6 +33,13 @@ paths: - rooms summary: List all rooms operationId: roomsList + parameters: + - in: query + name: labels + schema: + type: object + additionalProperties: + type: string responses: '200': description: OK @@ -318,6 +325,10 @@ components: type: string format: datetime example: "2021-03-07T21:56:34Z" + labels: + type: object + additionalProperties: + type: string RoomMount: type: object @@ -415,6 +426,10 @@ components: type: string example: CUSTOM_ENV: custom value + labels: + type: object + additionalProperties: + type: string mounts: type: array items: diff --git a/client/src/api/api.ts b/client/src/api/api.ts index d5bbc2d..e0b19f9 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -272,6 +272,12 @@ export interface RoomEntry { * @memberof RoomEntry */ 'created'?: string; + /** + * + * @type {{ [key: string]: string; }} + * @memberof RoomEntry + */ + 'labels'?: { [key: string]: string; }; } /** * @@ -490,6 +496,12 @@ export interface RoomSettings { * @memberof RoomSettings */ 'envs'?: { [key: string]: string; }; + /** + * + * @type {{ [key: string]: string; }} + * @memberof RoomSettings + */ + 'labels'?: { [key: string]: string; }; /** * * @type {Array} @@ -1248,10 +1260,11 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration /** * * @summary List all rooms + * @param {{ [key: string]: string; }} [labels] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - roomsList: async (options: AxiosRequestConfig = {}): Promise => { + roomsList: async (labels?: { [key: string]: string; }, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/api/rooms`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -1264,6 +1277,12 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + if (labels !== undefined) { + for (let param of Object.keys(labels)) { + localVarQueryParameter[param] = labels?.[param]; + } + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -1387,11 +1406,12 @@ export const RoomsApiFp = function(configuration?: Configuration) { /** * * @summary List all rooms + * @param {{ [key: string]: string; }} [labels] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async roomsList(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.roomsList(options); + async roomsList(labels?: { [key: string]: string; }, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.roomsList(labels, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -1497,11 +1517,12 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath /** * * @summary List all rooms + * @param {{ [key: string]: string; }} [labels] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - roomsList(options?: any): AxiosPromise> { - return localVarFp.roomsList(options).then((request) => request(axios, basePath)); + roomsList(labels?: { [key: string]: string; }, options?: any): AxiosPromise> { + return localVarFp.roomsList(labels, options).then((request) => request(axios, basePath)); }, }; }; @@ -1624,12 +1645,13 @@ export class RoomsApi extends BaseAPI { /** * * @summary List all rooms + * @param {{ [key: string]: string; }} [labels] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof RoomsApi */ - public roomsList(options?: AxiosRequestConfig) { - return RoomsApiFp(this.configuration).roomsList(options).then((request) => request(this.axios, this.basePath)); + public roomsList(labels?: { [key: string]: string; }, options?: AxiosRequestConfig) { + return RoomsApiFp(this.configuration).roomsList(labels, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/client/src/components/RoomInfo.vue b/client/src/components/RoomInfo.vue index 6a775a5..d65c01d 100644 --- a/client/src/components/RoomInfo.vue +++ b/client/src/components/RoomInfo.vue @@ -156,14 +156,27 @@ -
Mounts
- - - + + +
Resources
diff --git a/docs/lables.md b/docs/lables.md index 2088a1f..6f03fc4 100644 --- a/docs/lables.md +++ b/docs/lables.md @@ -1,6 +1,6 @@ # custom labels -You can add custom lables for every room. +You can add custom labels for every room. Example: Expose port 3000 from every room under `3000-(room-name)`: diff --git a/internal/api/rooms.go b/internal/api/rooms.go index c5cdcda..78eefe5 100644 --- a/internal/api/rooms.go +++ b/internal/api/rooms.go @@ -3,14 +3,28 @@ package api import ( "encoding/json" "net/http" + "strings" "github.com/go-chi/chi/v5" + "github.com/m1k1o/neko-rooms/internal/room" "github.com/m1k1o/neko-rooms/internal/types" ) func (manager *ApiManagerCtx) roomsList(w http.ResponseWriter, r *http.Request) { - response, err := manager.rooms.List() + labelsMap := map[string]string{} + for key, value := range r.URL.Query() { + key = strings.ToLower(key) + + if !room.CheckLabelKey(key) { + http.Error(w, "invalid label name, allowed characters: [a-z0-9.-]", 400) + return + } + + labelsMap[key] = value[0] + } + + response, err := manager.rooms.List(labelsMap) if err != nil { http.Error(w, err.Error(), 500) return diff --git a/internal/room/containers.go b/internal/room/containers.go index bbf41db..3c76e86 100644 --- a/internal/room/containers.go +++ b/internal/room/containers.go @@ -28,6 +28,7 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container) Running: container.State == "running", Status: container.Status, Created: time.Unix(container.Created, 0), + Labels: labels.UserDefined, } if labels.Mux { @@ -37,35 +38,23 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container) return entry, nil } -func (manager *RoomManagerCtx) listContainers() ([]dockerTypes.Container, error) { +func (manager *RoomManagerCtx) listContainers(labels map[string]string) ([]dockerTypes.Container, error) { args := filters.NewArgs( - filters.Arg("label", "m1k1o.neko_rooms.instance"), + filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", manager.config.InstanceName)), ) - containers, err := manager.client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{ + for key, val := range labels { + args.Add("label", fmt.Sprintf("m1k1o.neko_rooms.x-%s=%s", key, val)) + } + + return manager.client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{ All: true, Filters: args, }) - - if err != nil { - return nil, err - } - - result := []dockerTypes.Container{} - for _, container := range containers { - val, ok := container.Labels["m1k1o.neko_rooms.instance"] - if !ok || val != manager.config.InstanceName { - continue - } - - result = append(result, container) - } - - return result, nil } func (manager *RoomManagerCtx) containerFilter(args filters.Args) (*dockerTypes.Container, error) { - args.Add("label", "m1k1o.neko_rooms.instance") + args.Add("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", manager.config.InstanceName)) containers, err := manager.client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{ All: true, @@ -81,12 +70,6 @@ func (manager *RoomManagerCtx) containerFilter(args filters.Args) (*dockerTypes. } container := containers[0] - - val, ok := container.Labels["m1k1o.neko_rooms.instance"] - if !ok || val != manager.config.InstanceName { - return nil, fmt.Errorf("this container does not belong to neko_rooms") - } - return &container, nil } diff --git a/internal/room/labels.go b/internal/room/labels.go index 34f752a..4617172 100644 --- a/internal/room/labels.go +++ b/internal/room/labels.go @@ -2,11 +2,15 @@ package room import ( "fmt" + "regexp" "strconv" + "strings" "github.com/m1k1o/neko-rooms/internal/types" ) +var labelRegex = regexp.MustCompile(`^[a-z0-9.-]+$`) + type RoomLabels struct { Name string URL string @@ -15,6 +19,7 @@ type RoomLabels struct { NekoImage string BrowserPolicy *BrowserPolicyLabels + UserDefined map[string]string } type BrowserPolicyLabels struct { @@ -101,6 +106,14 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab } } + // extract user defined labels + userDefined := map[string]string{} + for key, val := range labels { + if strings.HasPrefix(key, "m1k1o.neko_rooms.x-") { + userDefined[strings.TrimPrefix(key, "m1k1o.neko_rooms.x-")] = val + } + } + return &RoomLabels{ Name: name, URL: url, @@ -109,6 +122,7 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab Epr: epr, BrowserPolicy: browserPolicy, + UserDefined: userDefined, }, nil } @@ -133,5 +147,16 @@ func (manager *RoomManagerCtx) serializeLabels(labels RoomLabels) map[string]str labelsMap["m1k1o.neko_rooms.browser_policy.path"] = labels.BrowserPolicy.Path } + for key, val := range labels.UserDefined { + // to lowercase + key = strings.ToLower(key) + + labelsMap[fmt.Sprintf("m1k1o.neko_rooms.x-%s", key)] = val + } + return labelsMap } + +func CheckLabelKey(name string) bool { + return labelRegex.MatchString(name) +} diff --git a/internal/room/manager.go b/internal/room/manager.go index 1695a8b..9f43249 100644 --- a/internal/room/manager.go +++ b/internal/room/manager.go @@ -6,6 +6,7 @@ import ( "encoding/csv" "encoding/json" "fmt" + "net/url" "os" "path" "path/filepath" @@ -62,13 +63,13 @@ func (manager *RoomManagerCtx) Config() types.RoomsConfig { } } -func (manager *RoomManagerCtx) List() ([]types.RoomEntry, error) { - containers, err := manager.listContainers() +func (manager *RoomManagerCtx) List(labels map[string]string) ([]types.RoomEntry, error) { + containers, err := manager.listContainers(labels) if err != nil { return nil, err } - result := []types.RoomEntry{} + result := make([]types.RoomEntry, 0, len(containers)) for _, container := range containers { entry, err := manager.containerToEntry(container) if err != nil { @@ -174,6 +175,7 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro NekoImage: settings.NekoImage, BrowserPolicy: browserPolicyLabels, + UserDefined: settings.Labels, }) // @@ -652,9 +654,10 @@ func (manager *RoomManagerCtx) GetSettings(id string) (*types.RoomSettings, erro Name: labels.Name, NekoImage: labels.NekoImage, MaxConnections: labels.Epr.Max - labels.Epr.Min + 1, + Labels: labels.UserDefined, Mounts: mounts, - BrowserPolicy: browserPolicy, Resources: roomResources, + BrowserPolicy: browserPolicy, } if labels.Mux { @@ -678,7 +681,7 @@ func (manager *RoomManagerCtx) GetStats(id string) (*types.RoomStats, error) { } output, err := manager.containerExec(id, []string{ - "wget", "-q", "-O-", "http://127.0.0.1:8080/stats?pwd=" + settings.AdminPass, + "wget", "-q", "-O-", "http://127.0.0.1:8080/stats?pwd=" + url.QueryEscape(settings.AdminPass), }) if err != nil { return nil, err diff --git a/internal/room/ports.go b/internal/room/ports.go index 9395298..9219b3f 100644 --- a/internal/room/ports.go +++ b/internal/room/ports.go @@ -44,7 +44,7 @@ func (manager *RoomManagerCtx) allocatePorts(sum uint16) (EprPorts, error) { } func (manager *RoomManagerCtx) getUsedPorts() ([]EprPorts, error) { - containers, err := manager.listContainers() + containers, err := manager.listContainers(nil) if err != nil { return nil, err } diff --git a/internal/types/room.go b/internal/types/room.go index e5038af..9ebf778 100644 --- a/internal/types/room.go +++ b/internal/types/room.go @@ -35,15 +35,16 @@ type RoomsConfig struct { } type RoomEntry struct { - ID string `json:"id"` - URL string `json:"url"` - Name string `json:"name"` - NekoImage string `json:"neko_image"` - IsOutdated bool `json:"is_outdated"` - MaxConnections uint16 `json:"max_connections"` // 0 when using mux - Running bool `json:"running"` - Status string `json:"status"` - Created time.Time `json:"created"` + ID string `json:"id"` + URL string `json:"url"` + Name string `json:"name"` + NekoImage string `json:"neko_image"` + IsOutdated bool `json:"is_outdated"` + MaxConnections uint16 `json:"max_connections"` // 0 when using mux + Running bool `json:"running"` + Status string `json:"status"` + Created time.Time `json:"created"` + Labels map[string]string `json:"labels,omitempty"` } type MountType string @@ -94,6 +95,7 @@ type RoomSettings struct { BroadcastPipeline string `json:"broadcast_pipeline,omitempty"` Envs map[string]string `json:"envs"` + Labels map[string]string `json:"labels"` Mounts []RoomMount `json:"mounts"` Resources RoomResources `json:"resources"` @@ -280,7 +282,7 @@ type RoomMember struct { type RoomManager interface { Config() RoomsConfig - List() ([]RoomEntry, error) + List(labels map[string]string) ([]RoomEntry, error) FindByName(name string) (*RoomEntry, error) Create(settings RoomSettings) (string, error)