add labels filter support to API.

This commit is contained in:
Miroslav Šedivý
2023-09-30 17:26:44 +02:00
parent 7b38250698
commit e7b9d39a7c
10 changed files with 136 additions and 59 deletions
+15
View File
@@ -33,6 +33,13 @@ paths:
- rooms - rooms
summary: List all rooms summary: List all rooms
operationId: roomsList operationId: roomsList
parameters:
- in: query
name: labels
schema:
type: object
additionalProperties:
type: string
responses: responses:
'200': '200':
description: OK description: OK
@@ -318,6 +325,10 @@ components:
type: string type: string
format: datetime format: datetime
example: "2021-03-07T21:56:34Z" example: "2021-03-07T21:56:34Z"
labels:
type: object
additionalProperties:
type: string
RoomMount: RoomMount:
type: object type: object
@@ -415,6 +426,10 @@ components:
type: string type: string
example: example:
CUSTOM_ENV: custom value CUSTOM_ENV: custom value
labels:
type: object
additionalProperties:
type: string
mounts: mounts:
type: array type: array
items: items:
+29 -7
View File
@@ -272,6 +272,12 @@ export interface RoomEntry {
* @memberof RoomEntry * @memberof RoomEntry
*/ */
'created'?: string; 'created'?: string;
/**
*
* @type {{ [key: string]: string; }}
* @memberof RoomEntry
*/
'labels'?: { [key: string]: string; };
} }
/** /**
* *
@@ -490,6 +496,12 @@ export interface RoomSettings {
* @memberof RoomSettings * @memberof RoomSettings
*/ */
'envs'?: { [key: string]: string; }; 'envs'?: { [key: string]: string; };
/**
*
* @type {{ [key: string]: string; }}
* @memberof RoomSettings
*/
'labels'?: { [key: string]: string; };
/** /**
* *
* @type {Array<RoomMount>} * @type {Array<RoomMount>}
@@ -1248,10 +1260,11 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
/** /**
* *
* @summary List all rooms * @summary List all rooms
* @param {{ [key: string]: string; }} [labels]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
roomsList: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { roomsList: async (labels?: { [key: string]: string; }, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/rooms`; const localVarPath = `/api/rooms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -1264,6 +1277,12 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
const localVarHeaderParameter = {} as any; const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any; const localVarQueryParameter = {} as any;
if (labels !== undefined) {
for (let param of Object.keys(labels)) {
localVarQueryParameter[param] = labels?.[param];
}
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -1387,11 +1406,12 @@ export const RoomsApiFp = function(configuration?: Configuration) {
/** /**
* *
* @summary List all rooms * @summary List all rooms
* @param {{ [key: string]: string; }} [labels]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async roomsList(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<RoomEntry>>> { async roomsList(labels?: { [key: string]: string; }, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<RoomEntry>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomsList(options); const localVarAxiosArgs = await localVarAxiosParamCreator.roomsList(labels, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
} }
@@ -1497,11 +1517,12 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
/** /**
* *
* @summary List all rooms * @summary List all rooms
* @param {{ [key: string]: string; }} [labels]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
roomsList(options?: any): AxiosPromise<Array<RoomEntry>> { roomsList(labels?: { [key: string]: string; }, options?: any): AxiosPromise<Array<RoomEntry>> {
return localVarFp.roomsList(options).then((request) => request(axios, basePath)); return localVarFp.roomsList(labels, options).then((request) => request(axios, basePath));
}, },
}; };
}; };
@@ -1624,12 +1645,13 @@ export class RoomsApi extends BaseAPI {
/** /**
* *
* @summary List all rooms * @summary List all rooms
* @param {{ [key: string]: string; }} [labels]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof RoomsApi * @memberof RoomsApi
*/ */
public roomsList(options?: AxiosRequestConfig) { public roomsList(labels?: { [key: string]: string; }, options?: AxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomsList(options).then((request) => request(this.axios, this.basePath)); return RoomsApiFp(this.configuration).roomsList(labels, options).then((request) => request(this.axios, this.basePath));
} }
} }
+21 -8
View File
@@ -156,14 +156,27 @@
</template> </template>
</v-simple-table> </v-simple-table>
<div class="my-3 headline">Mounts</div> <template v-if="settings.labels && Object.keys(settings.labels).length > 0">
<v-simple-table> <div class="my-3 headline">Labels</div>
<template v-slot:default> <v-simple-table>
<tbody> <template v-slot:default>
<tr v-for="({ type, host_path, container_path }, index) in settings.mounts" :key="index"><td style="width:50%;">{{ host_path }} <v-chip small>{{ type }}</v-chip></td><td>{{ container_path }}</td></tr> <tbody>
</tbody> <tr v-for="(val, key) in settings.labels" :key="key"><th style="width:50%;">{{ key }}</th><td>{{ val }}</td></tr>
</template> </tbody>
</v-simple-table> </template>
</v-simple-table>
</template>
<template v-if="settings.mounts && settings.mounts.length > 0">
<div class="my-3 headline">Mounts</div>
<v-simple-table>
<template v-slot:default>
<tbody>
<tr v-for="({ type, host_path, container_path }, index) in settings.mounts" :key="index"><td style="width:50%;">{{ host_path }} <v-chip small>{{ type }}</v-chip></td><td>{{ container_path }}</td></tr>
</tbody>
</template>
</v-simple-table>
</template>
<div class="my-3 headline">Resources</div> <div class="my-3 headline">Resources</div>
<v-simple-table> <v-simple-table>
+1 -1
View File
@@ -1,6 +1,6 @@
# custom labels # 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)`: Example: Expose port 3000 from every room under `3000-(room-name)`:
+15 -1
View File
@@ -3,14 +3,28 @@ package api
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/m1k1o/neko-rooms/internal/room"
"github.com/m1k1o/neko-rooms/internal/types" "github.com/m1k1o/neko-rooms/internal/types"
) )
func (manager *ApiManagerCtx) roomsList(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
+9 -26
View File
@@ -28,6 +28,7 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container)
Running: container.State == "running", Running: container.State == "running",
Status: container.Status, Status: container.Status,
Created: time.Unix(container.Created, 0), Created: time.Unix(container.Created, 0),
Labels: labels.UserDefined,
} }
if labels.Mux { if labels.Mux {
@@ -37,35 +38,23 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container)
return entry, nil return entry, nil
} }
func (manager *RoomManagerCtx) listContainers() ([]dockerTypes.Container, error) { func (manager *RoomManagerCtx) listContainers(labels map[string]string) ([]dockerTypes.Container, error) {
args := filters.NewArgs( 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, All: true,
Filters: args, 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) { 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{ containers, err := manager.client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{
All: true, All: true,
@@ -81,12 +70,6 @@ func (manager *RoomManagerCtx) containerFilter(args filters.Args) (*dockerTypes.
} }
container := containers[0] 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 return &container, nil
} }
+25
View File
@@ -2,11 +2,15 @@ package room
import ( import (
"fmt" "fmt"
"regexp"
"strconv" "strconv"
"strings"
"github.com/m1k1o/neko-rooms/internal/types" "github.com/m1k1o/neko-rooms/internal/types"
) )
var labelRegex = regexp.MustCompile(`^[a-z0-9.-]+$`)
type RoomLabels struct { type RoomLabels struct {
Name string Name string
URL string URL string
@@ -15,6 +19,7 @@ type RoomLabels struct {
NekoImage string NekoImage string
BrowserPolicy *BrowserPolicyLabels BrowserPolicy *BrowserPolicyLabels
UserDefined map[string]string
} }
type BrowserPolicyLabels struct { 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{ return &RoomLabels{
Name: name, Name: name,
URL: url, URL: url,
@@ -109,6 +122,7 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab
Epr: epr, Epr: epr,
BrowserPolicy: browserPolicy, BrowserPolicy: browserPolicy,
UserDefined: userDefined,
}, nil }, nil
} }
@@ -133,5 +147,16 @@ func (manager *RoomManagerCtx) serializeLabels(labels RoomLabels) map[string]str
labelsMap["m1k1o.neko_rooms.browser_policy.path"] = labels.BrowserPolicy.Path 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 return labelsMap
} }
func CheckLabelKey(name string) bool {
return labelRegex.MatchString(name)
}
+8 -5
View File
@@ -6,6 +6,7 @@ import (
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@@ -62,13 +63,13 @@ func (manager *RoomManagerCtx) Config() types.RoomsConfig {
} }
} }
func (manager *RoomManagerCtx) List() ([]types.RoomEntry, error) { func (manager *RoomManagerCtx) List(labels map[string]string) ([]types.RoomEntry, error) {
containers, err := manager.listContainers() containers, err := manager.listContainers(labels)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := []types.RoomEntry{} result := make([]types.RoomEntry, 0, len(containers))
for _, container := range containers { for _, container := range containers {
entry, err := manager.containerToEntry(container) entry, err := manager.containerToEntry(container)
if err != nil { if err != nil {
@@ -174,6 +175,7 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
NekoImage: settings.NekoImage, NekoImage: settings.NekoImage,
BrowserPolicy: browserPolicyLabels, BrowserPolicy: browserPolicyLabels,
UserDefined: settings.Labels,
}) })
// //
@@ -652,9 +654,10 @@ func (manager *RoomManagerCtx) GetSettings(id string) (*types.RoomSettings, erro
Name: labels.Name, Name: labels.Name,
NekoImage: labels.NekoImage, NekoImage: labels.NekoImage,
MaxConnections: labels.Epr.Max - labels.Epr.Min + 1, MaxConnections: labels.Epr.Max - labels.Epr.Min + 1,
Labels: labels.UserDefined,
Mounts: mounts, Mounts: mounts,
BrowserPolicy: browserPolicy,
Resources: roomResources, Resources: roomResources,
BrowserPolicy: browserPolicy,
} }
if labels.Mux { if labels.Mux {
@@ -678,7 +681,7 @@ func (manager *RoomManagerCtx) GetStats(id string) (*types.RoomStats, error) {
} }
output, err := manager.containerExec(id, []string{ 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 { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -44,7 +44,7 @@ func (manager *RoomManagerCtx) allocatePorts(sum uint16) (EprPorts, error) {
} }
func (manager *RoomManagerCtx) getUsedPorts() ([]EprPorts, error) { func (manager *RoomManagerCtx) getUsedPorts() ([]EprPorts, error) {
containers, err := manager.listContainers() containers, err := manager.listContainers(nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+12 -10
View File
@@ -35,15 +35,16 @@ type RoomsConfig struct {
} }
type RoomEntry struct { type RoomEntry struct {
ID string `json:"id"` ID string `json:"id"`
URL string `json:"url"` URL string `json:"url"`
Name string `json:"name"` Name string `json:"name"`
NekoImage string `json:"neko_image"` NekoImage string `json:"neko_image"`
IsOutdated bool `json:"is_outdated"` IsOutdated bool `json:"is_outdated"`
MaxConnections uint16 `json:"max_connections"` // 0 when using mux MaxConnections uint16 `json:"max_connections"` // 0 when using mux
Running bool `json:"running"` Running bool `json:"running"`
Status string `json:"status"` Status string `json:"status"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Labels map[string]string `json:"labels,omitempty"`
} }
type MountType string type MountType string
@@ -94,6 +95,7 @@ type RoomSettings struct {
BroadcastPipeline string `json:"broadcast_pipeline,omitempty"` BroadcastPipeline string `json:"broadcast_pipeline,omitempty"`
Envs map[string]string `json:"envs"` Envs map[string]string `json:"envs"`
Labels map[string]string `json:"labels"`
Mounts []RoomMount `json:"mounts"` Mounts []RoomMount `json:"mounts"`
Resources RoomResources `json:"resources"` Resources RoomResources `json:"resources"`
@@ -280,7 +282,7 @@ type RoomMember struct {
type RoomManager interface { type RoomManager interface {
Config() RoomsConfig Config() RoomsConfig
List() ([]RoomEntry, error) List(labels map[string]string) ([]RoomEntry, error)
FindByName(name string) (*RoomEntry, error) FindByName(name string) (*RoomEntry, error)
Create(settings RoomSettings) (string, error) Create(settings RoomSettings) (string, error)