45 Commits

Author SHA1 Message Date
Miroslav Šedivý 8ab1cafa93 use latest traefik 2 version in docker compose, fixes #174. 2026-04-25 15:18:54 +02:00
Miroslav Šedivý aa3a09fe33 Add AdGuard AdBlocker and update uBlock Origin Lite configuration. 2026-03-29 20:01:23 +02:00
Miroslav Šedivý b238d4da03 WithAPIVersionNegotiation enables automatic API version negotiation for the client. With this option enabled, the client automatically negotiates the API version to use when making requests. API version negotiation is performed on the first request; subsequent requests do not re-negotiate. #173. 2025-12-11 18:08:14 +01:00
Miroslav Šedivý d59605ff9a remove DOCKER_API_VERSION from dockerfile, fixes #172. 2025-11-30 21:24:48 +01:00
H1ghSyst3m 5028a28255 NGINX WebSocket and Proxy Improvements (#170) 2025-09-26 09:00:39 +02:00
Miroslav Šedivý e2688cb853 Upgrade to go1.24 (#168)
* upgrade to go1.24.

* code cleanup.
2025-09-17 20:52:40 +02:00
Daisuke Bauza b8255b0f62 fixed nginx config and all configs (#167)
Basically the nginx config that was used will have this bug called "peer data channel closed" that will happen every single 3 minutes or so and then it will automatically reconnect the nginx file basically whenever someone was in HTTPS instead of HTTP it will just not trust it but with the nginx config and adding the headers the browser will trust the WS connection
2025-09-17 20:48:56 +02:00
Miroslav Šedivý da890b40fc implicit control is disabled by default in legacy mode. 2025-05-29 11:59:51 +02:00
Miroslav Šedivý 893b7270d6 fix v3 pipelines. 2025-05-29 11:54:47 +02:00
Miroslav Šedivý d09e48756c fix parse room env. 2025-05-29 11:53:40 +02:00
Miroslav Šedivý 4232861d90 use slices.Contains. 2025-05-29 11:51:31 +02:00
Miroslav Šedivý 64eab2db4c v3: add NEKO_SESSION_CONTROL_PROTECTION. 2025-05-29 11:48:46 +02:00
zerohh 1013e80034 Add room paused state (#163)
* add room_Paused from cniu6

* fix "OpenAPi.yml" and api-request

* remove unpaused event and change start event to unpause as well.

* update openapi bindings.

* do not wait for ready when unpausing container.

* fix room create.

* add paused state to Lobby.

* fix roomRecreate.

---------

Co-authored-by: zerohh <zerohh666@163.com>
Co-authored-by: Miroslav Šedivý <sedivy.miro@gmail.com>
2025-05-28 15:54:27 +02:00
Miroslav Šedivý 8256bcaf99 traefik install include all images. 2025-04-04 23:08:11 +02:00
Miroslav Šedivý c3e97330a6 move nginx installation to community folder. 2025-04-04 23:06:01 +02:00
Miroslav Šedivý d0776e85fd change version api label. 2025-04-03 22:01:18 +02:00
Miroslav Šedivý ab33ca1e42 fix typo. 2025-04-03 21:20:47 +02:00
Miroslav Šedivý 2f6804a8a5 change to ghcr. 2025-04-03 21:17:09 +02:00
Miroslav Šedivý 2d2dc09433 update workflows. 2025-04-03 21:17:09 +02:00
shanurrahman 78b154f6bc Scripts for installing the app in amazon linux (#156)
* docs: add community installation scripts section

Add a new section in README.md to highlight community-contributed installation scripts for non-Ubuntu systems. Include references to community documentation and mention support for distributions like Arch Linux and Fedora. This improves visibility of alternative installation options for users running different Linux distributions.

* Adding community scripts for installation
2025-02-12 19:27:58 +01:00
Miroslav Šedivý 11ac1d4a89 update build workflow. 2025-02-10 16:04:53 +01:00
shanurrahman e5c37c7f44 chore: add tryiris-typescript to gitignore (#153)
Exclude tryiris-typescript directory from version control
2025-02-10 15:57:22 +01:00
H1ghSyst3m 30d9dc7fcd feat: add NGINX installer and update README (#146)
* feat: add NGINX installer script for zero-knowledge setup

- Created a new installer script for NGINX-based setups.
- The script automates Docker, NGINX, Let's Encrypt, and Neko Rooms configuration.
- Designed for users who prefer NGINX as a reverse proxy instead of Traefik.

* docs: update README to include zero-knowledge installation instructions with NGINX

* feat: enhance IP retrieval method and add user prompt for domain continuation

* docs: update README to streamline zero-knowledge installation instructions and clarify NGINX usage

* feat: update IP retrieval fallback service to use checkip.amazonaws.com
2025-01-14 23:04:47 +01:00
Miroslav Šedivý fd03fb67d2 fix docker keyword AS. 2025-01-11 11:09:11 +01:00
Miroslav Šedivý ef4385d471 fix ip for dev. 2024-09-17 19:24:50 +02:00
Miroslav Šedivý 0aa712278f events logs. 2024-09-17 19:24:34 +02:00
Miroslav Šedivý 3aeaf6acf9 fix bug when api version would be set to 0 if there is no m1k1o.neko_rooms.api_version label and org.opencontainers.image. contains different URL. 2024-09-13 20:14:57 +02:00
Miroslav Šedivý e61f6bf624 fix mount reactiveness, #128. 2024-06-17 22:00:06 +02:00
Miroslav Šedivý 76c0d28c28 support docker compose, fixes #127. 2024-05-31 21:30:55 +02:00
Miroslav Šedivý 44fa2e06e4 Metrics (#115)
* metrics add totalRooms and runningRooms.

* load initial metrics.

* fix.
2024-03-03 17:31:18 +01:00
Miroslav Šedivý 74fe2d7eab add stop timeout to config. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý a844a9f9d2 add sse events to client. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý afa67ac537 move events out of the rooms. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý d7f7acc7f3 room events optional sse. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý 2f7aa7ed13 events fix logging level. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý bf83709880 fix proxy. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý 77695d42a3 fix double logging. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý 2ac5e83448 wait only for ready. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý 38e4cab3fe proxy use room manager. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý b62ac1097c container labels and room event type. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý 7d70f7ac9b fix events. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý 4d2393c457 add is ready to API. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý ab8c6a53ec WIP. 2023-12-26 16:17:55 +01:00
Miroslav Šedivý a6169da135 interface{} to any. 2023-11-25 22:54:35 +01:00
Miroslav Šedivý ffeef5e887 fix pprof. 2023-11-25 15:37:08 +01:00
62 changed files with 3211 additions and 1163 deletions
+44 -71
View File
@@ -7,100 +7,73 @@ on:
tags:
- 'v*'
env:
IMAGE_NAME: m1k1o/neko-rooms
jobs:
build-client:
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: ./client/package-lock.json
-
name: Install dependencies
run: cd ./client && npm install
-
name: Build
run: cd ./client && npm run build
-
name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: client
path: ./client/dist
name: Build Client Artifacts
uses: ./.github/workflows/client_build.yml
build-docker:
needs: build-client
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
needs: build-client
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v4
-
name: Download client artifact
uses: actions/download-artifact@v3
with:
fetch-depth: 0
- name: Download client dist
uses: actions/download-artifact@v4
with:
name: client
path: ./client/dist
-
name: Remove client stage from Dockerfile
# Change dockerfile: remove first stage - everything between # STAGE 1 and # STAGE 2
# Replace "--from=frontend /src/dist/" with "./client/dist/"
run: |
sed -i '/# STAGE 1/,/# STAGE 2/d' ./Dockerfile
sed -i 's/--from=frontend \/src\/dist\//.\/client\/dist\//g' ./Dockerfile
-
name: Set up QEMU
path: client/dist
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
id: buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
-
name: Extract metadata (tags, labels) for Docker
- name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE_NAME }}
images: |
docker.io/${{ github.repository }}
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
-
name: Log in to the Container registry
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v5
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
# Turns out the build-push-action runs in a "GitHub" context by default.
# At the very least, it means pulling down your repository as part of pre-Dockerfile command.
# You can change it to the "file" context by using:
context: .
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_ACCESS_TOKEN }}
- name: Remove client stage from Dockerfile
# Change dockerfile: remove first stage - everything between # STAGE 1 and # STAGE 2
# Replace "--from=frontend /src/dist/" with "./client/dist/"
run: |
sed -i '/# STAGE 1/,/# STAGE 2/d' ./Dockerfile
sed -i 's/--from=frontend \/src\/dist\//.\/client\/dist\//g' ./Dockerfile
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
+45
View File
@@ -0,0 +1,45 @@
name: Build Client
on:
workflow_call:
inputs:
with-artifact:
required: false
type: boolean
default: true
description: |
If true, the build artifacts will be uploaded as a GitHub Actions artifact.
This is useful for debugging and testing purposes. If false, the artifacts
will not be uploaded. This is useful for test builds where you don't need
the artifacts.
jobs:
build-client:
name: Build Client
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: client/package-lock.json
- name: Install dependencies
working-directory: ./client
run: npm ci
- name: Build client
working-directory: ./client
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: ${{ inputs.with-artifact }}
with:
name: client
path: client/dist
+19
View File
@@ -0,0 +1,19 @@
name: Test Client
on:
pull_request:
branches:
- master
paths:
- client/**
- .github/workflows/client_build.yml
- .github/workflows/client_test.yml
jobs:
test-client:
name: Test Client
uses: ./.github/workflows/client_build.yml
with:
# Do not upload artifacts for test builds
with-artifact: false
secrets: inherit
+2 -3
View File
@@ -1,7 +1,7 @@
#
# STAGE 1: build static web files
#
FROM node:20-bookworm-slim as frontend
FROM node:20-bookworm-slim AS frontend
WORKDIR /src
#
@@ -17,7 +17,7 @@ RUN npm run build
#
# STAGE 2: build executable binary
#
FROM golang:1.21-bullseye as builder
FROM golang:1.24-bookworm AS builder
WORKDIR /app
COPY . .
@@ -31,7 +31,6 @@ FROM scratch
COPY --from=builder /app/bin/neko_rooms /app/bin/neko_rooms
COPY --from=frontend /src/dist/ /var/www
ENV DOCKER_API_VERSION=1.39
ENV NEKO_ROOMS_BIND=:8080
ENV NEKO_ROOMS_ADMIN_STATIC=/var/www
+33 -5
View File
@@ -65,6 +65,7 @@ paths:
default: true
description: Start room after creation
requestBody:
required: true
content:
application/json:
schema:
@@ -253,6 +254,25 @@ paths:
description: Room not found
'500':
description: Internal server error
/api/rooms/{roomId}/pause:
post:
tags:
- rooms
summary: Pause room
operationId: roomPause
parameters:
- in: path
name: roomId
required: true
schema:
type: string
responses:
'204':
description: OK
'404':
description: Room not found
'500':
description: Internal server error
/api/rooms/{roomId}/recreate:
post:
tags:
@@ -274,6 +294,7 @@ paths:
Whether the room should be started or stopped after
recreation, default is the current state of the room.
requestBody:
required: true
content:
application/json:
schema:
@@ -324,6 +345,7 @@ paths:
summary: Start new pull
operationId: pullStart
requestBody:
required: true
content:
application/json:
schema:
@@ -378,7 +400,7 @@ components:
type: array
items:
type: string
example: m1k1o/neko:latest
example: ghcr.io/m1k1o/neko/firefox:latest
storage_enabled:
type: boolean
example: true
@@ -400,7 +422,7 @@ components:
example: foobar
neko_image:
type: string
example: m1k1o/neko:latest
example: ghcr.io/m1k1o/neko/firefox:latest
is_outdated:
type: boolean
example: true
@@ -411,6 +433,12 @@ components:
running:
type: boolean
example: true
paused:
type: boolean
example: false
is_ready:
type: boolean
example: true
status:
type: string
example: Up 2 seconds
@@ -475,7 +503,7 @@ components:
example: foobar
neko_image:
type: string
example: m1k1o/neko:latest
example: ghcr.io/m1k1o/neko/firefox:latest
max_connections:
type: number
default: 10
@@ -647,7 +675,7 @@ components:
properties:
neko_image:
type: string
example: m1k1o/neko:latest
example: ghcr.io/m1k1o/neko/firefox:latest
registry_user:
type: string
registry_pass:
@@ -658,7 +686,7 @@ components:
properties:
status:
type: string
example: m1k1o/neko:latest
example: ghcr.io/m1k1o/neko/firefox:latest
progressDetail:
type: object
properties:
+11 -7
View File
@@ -16,20 +16,24 @@ Simple room management system for [n.eko](https://github.com/m1k1o/neko). Self h
<img src="https://github.com/m1k1o/neko-rooms/raw/master/docs/neko.gif" alt="n.eko">
</div>
## Zero-knowledge installation (with HTTPS and Traefik)
## Zero-knowledge installation (with HTTPS)
If you don't have any clue about docker and stuff but only want to have fun with friends in a shared browser, we got you covered!
No experience with Docker and reverse proxy? No problem! Follow these steps to set up your Neko Rooms quickly and securely:
- Rent a VPS with public IP and OS Ubuntu.
- Get a domain name pointing to your IP (you can even get some for free).
- Run install script and follow instructions.
- Secure using HTTPs thanks to Let's Encrypt and Traefik.
- Secure using HTTPs thanks to Let's Encrypt and Traefik or NGINX.
```bash
wget -O neko-rooms.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/install
sudo bash neko-rooms.sh
wget -O neko-rooms-traefik.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/install
sudo bash neko-rooms-traefik.sh
```
### Community Installation Scripts
We have community-contributed installation scripts available. Check out our [community installation guides](./community/README.md) for instructions on installing neko-rooms on various Linux distributions. These scripts are maintained by the community and support different Linux distributions like Arch Linux, Fedora, and more.
## How to start
If you want to use Traefik as reverse proxy, visit [installation guide for traefik as reverse proxy](./traefik/).
@@ -41,8 +45,8 @@ Otherwise modify variables in `docker-compose.yml` and just run `docker-compose
You need to pull all your images, that you want to use with neko-room. Otherwise, you might get this error: `Error response from daemon: No such image:` (see issue #1).
```sh
docker pull m1k1o/neko:firefox
docker pull m1k1o/neko:chromium
docker pull ghcr.io/m1k1o/neko/firefox
docker pull ghcr.io/m1k1o/neko/chromium
# etc...
```
+1 -1
View File
@@ -36,7 +36,7 @@
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
import { Vue, Component } from 'vue-property-decorator'
import Home from './views/Home.vue'
@Component({
+17
View File
@@ -5,5 +5,22 @@ api.ts
base.ts
common.ts
configuration.ts
docs/BrowserPolicy.md
docs/BrowserPolicyContent.md
docs/BrowserPolicyExtension.md
docs/ConfigApi.md
docs/DefaultApi.md
docs/PullLayer.md
docs/PullLayerProgressDetail.md
docs/PullStart.md
docs/PullStatus.md
docs/RoomEntry.md
docs/RoomMember.md
docs/RoomMount.md
docs/RoomResources.md
docs/RoomSettings.md
docs/RoomStats.md
docs/RoomsApi.md
docs/RoomsConfig.md
git_push.sh
index.ts
+1 -1
View File
@@ -1 +1 @@
7.1.0-SNAPSHOT
7.14.0-SNAPSHOT
+231 -110
View File
@@ -14,14 +14,14 @@
import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base';
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';
/**
*
@@ -260,6 +260,18 @@ export interface RoomEntry {
* @memberof RoomEntry
*/
'running'?: boolean;
/**
*
* @type {boolean}
* @memberof RoomEntry
*/
'paused'?: boolean;
/**
*
* @type {boolean}
* @memberof RoomEntry
*/
'is_ready'?: boolean;
/**
*
* @type {string}
@@ -395,7 +407,7 @@ export interface RoomResources {
*/
export interface RoomSettings {
/**
*
* if not set, version is taken from neko_image
* @type {number}
* @memberof RoomSettings
*/
@@ -650,7 +662,7 @@ export const ConfigApiAxiosParamCreator = function (configuration?: Configuratio
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomsConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomsConfig: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/config/rooms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -690,9 +702,11 @@ export const ConfigApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomsConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomsConfig>> {
async roomsConfig(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomsConfig>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomsConfig(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['ConfigApi.roomsConfig']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@@ -710,7 +724,7 @@ export const ConfigApiFactory = function (configuration?: Configuration, basePat
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomsConfig(options?: any): AxiosPromise<RoomsConfig> {
roomsConfig(options?: RawAxiosRequestConfig): AxiosPromise<RoomsConfig> {
return localVarFp.roomsConfig(options).then((request) => request(axios, basePath));
},
};
@@ -730,7 +744,7 @@ export class ConfigApi extends BaseAPI {
* @throws {RequiredError}
* @memberof ConfigApi
*/
public roomsConfig(options?: AxiosRequestConfig) {
public roomsConfig(options?: RawAxiosRequestConfig) {
return ConfigApiFp(this.configuration).roomsConfig(options).then((request) => request(this.axios, this.basePath));
}
}
@@ -746,11 +760,13 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
/**
*
* @summary Start new pull
* @param {PullStart} [pullStart]
* @param {PullStart} pullStart
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStart: async (pullStart?: PullStart, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
pullStart: async (pullStart: PullStart, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'pullStart' is not null or undefined
assertParamExists('pullStart', 'pullStart', pullStart)
const localVarPath = `/api/pull`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -783,7 +799,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStatus: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
pullStatus: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/pull`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -813,7 +829,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStatusSSE: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
pullStatusSSE: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/pull/sse`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -843,7 +859,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStop: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
pullStop: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/pull`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -880,13 +896,15 @@ export const DefaultApiFp = function(configuration?: Configuration) {
/**
*
* @summary Start new pull
* @param {PullStart} [pullStart]
* @param {PullStart} pullStart
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async pullStart(pullStart?: PullStart, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PullStatus>> {
async pullStart(pullStart: PullStart, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PullStatus>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStart(pullStart, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['DefaultApi.pullStart']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -894,9 +912,11 @@ export const DefaultApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async pullStatus(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PullStatus>> {
async pullStatus(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PullStatus>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStatus(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['DefaultApi.pullStatus']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -904,9 +924,11 @@ export const DefaultApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async pullStatusSSE(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PullLayer>>> {
async pullStatusSSE(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PullLayer>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStatusSSE(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['DefaultApi.pullStatusSSE']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -914,9 +936,11 @@ export const DefaultApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async pullStop(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async pullStop(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStop(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['DefaultApi.pullStop']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@@ -931,11 +955,11 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
/**
*
* @summary Start new pull
* @param {PullStart} [pullStart]
* @param {PullStart} pullStart
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStart(pullStart?: PullStart, options?: any): AxiosPromise<PullStatus> {
pullStart(pullStart: PullStart, options?: RawAxiosRequestConfig): AxiosPromise<PullStatus> {
return localVarFp.pullStart(pullStart, options).then((request) => request(axios, basePath));
},
/**
@@ -944,7 +968,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStatus(options?: any): AxiosPromise<PullStatus> {
pullStatus(options?: RawAxiosRequestConfig): AxiosPromise<PullStatus> {
return localVarFp.pullStatus(options).then((request) => request(axios, basePath));
},
/**
@@ -953,7 +977,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStatusSSE(options?: any): AxiosPromise<Array<PullLayer>> {
pullStatusSSE(options?: RawAxiosRequestConfig): AxiosPromise<Array<PullLayer>> {
return localVarFp.pullStatusSSE(options).then((request) => request(axios, basePath));
},
/**
@@ -962,7 +986,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
pullStop(options?: any): AxiosPromise<void> {
pullStop(options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.pullStop(options).then((request) => request(axios, basePath));
},
};
@@ -978,12 +1002,12 @@ export class DefaultApi extends BaseAPI {
/**
*
* @summary Start new pull
* @param {PullStart} [pullStart]
* @param {PullStart} pullStart
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public pullStart(pullStart?: PullStart, options?: AxiosRequestConfig) {
public pullStart(pullStart: PullStart, options?: RawAxiosRequestConfig) {
return DefaultApiFp(this.configuration).pullStart(pullStart, options).then((request) => request(this.axios, this.basePath));
}
@@ -994,7 +1018,7 @@ export class DefaultApi extends BaseAPI {
* @throws {RequiredError}
* @memberof DefaultApi
*/
public pullStatus(options?: AxiosRequestConfig) {
public pullStatus(options?: RawAxiosRequestConfig) {
return DefaultApiFp(this.configuration).pullStatus(options).then((request) => request(this.axios, this.basePath));
}
@@ -1005,7 +1029,7 @@ export class DefaultApi extends BaseAPI {
* @throws {RequiredError}
* @memberof DefaultApi
*/
public pullStatusSSE(options?: AxiosRequestConfig) {
public pullStatusSSE(options?: RawAxiosRequestConfig) {
return DefaultApiFp(this.configuration).pullStatusSSE(options).then((request) => request(this.axios, this.basePath));
}
@@ -1016,7 +1040,7 @@ export class DefaultApi extends BaseAPI {
* @throws {RequiredError}
* @memberof DefaultApi
*/
public pullStop(options?: AxiosRequestConfig) {
public pullStop(options?: RawAxiosRequestConfig) {
return DefaultApiFp(this.configuration).pullStop(options).then((request) => request(this.axios, this.basePath));
}
}
@@ -1035,7 +1059,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
exportAsDockerCompose: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
exportAsDockerCompose: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/docker-compose.yaml`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -1062,12 +1086,14 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
/**
*
* @summary Create new room
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomCreate: async (start?: boolean, roomSettings?: RoomSettings, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomCreate: async (roomSettings: RoomSettings, start?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomSettings' is not null or undefined
assertParamExists('roomCreate', 'roomSettings', roomSettings)
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);
@@ -1105,7 +1131,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomGet: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomGet: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomGet', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}`
@@ -1139,7 +1165,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomGetByName: async (roomName: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomGetByName: async (roomName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomName' is not null or undefined
assertParamExists('roomGetByName', 'roomName', roomName)
const localVarPath = `/api/rooms/{roomName}/by-name`
@@ -1168,16 +1194,52 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @summary Recreate room
* @summary Pause room
* @param {string} roomId
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRecreate: async (roomId: string, start?: boolean, roomSettings?: RoomSettings, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomPause: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomPause', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}/pause`
.replace(`{${"roomId"}}`, encodeURIComponent(String(roomId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Recreate room
* @param {string} roomId
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRecreate: async (roomId: string, roomSettings: RoomSettings, start?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomRecreate', 'roomId', roomId)
// verify required parameter 'roomSettings' is not null or undefined
assertParamExists('roomRecreate', 'roomSettings', roomSettings)
const localVarPath = `/api/rooms/{roomId}/recreate`
.replace(`{${"roomId"}}`, encodeURIComponent(String(roomId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
@@ -1216,7 +1278,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRemove: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomRemove: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomRemove', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}`
@@ -1250,7 +1312,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRestart: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomRestart: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomRestart', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}/restart`
@@ -1284,7 +1346,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomSettings: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomSettings: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomSettings', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}/settings`
@@ -1318,7 +1380,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomStart: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomStart: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomStart', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}/start`
@@ -1352,7 +1414,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomStats: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomStats: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomStats', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}/stats`
@@ -1386,7 +1448,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomStop: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomStop: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'roomId' is not null or undefined
assertParamExists('roomStop', 'roomId', roomId)
const localVarPath = `/api/rooms/{roomId}/stop`
@@ -1420,7 +1482,7 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomsList: async (labels?: { [key: string]: string; }, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
roomsList: async (labels?: { [key: string]: string; }, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
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);
@@ -1434,8 +1496,8 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
const localVarQueryParameter = {} as any;
if (labels !== undefined) {
for (let param of Object.keys(labels)) {
localVarQueryParameter[param] = labels?.[param];
for (const [key, value] of Object.entries(labels)) {
localVarQueryParameter[key] = value;
}
}
@@ -1466,21 +1528,25 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async exportAsDockerCompose(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async exportAsDockerCompose(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.exportAsDockerCompose(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.exportAsDockerCompose']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Create new room
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomCreate(start?: boolean, roomSettings?: RoomSettings, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomCreate(start, roomSettings, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
async roomCreate(roomSettings: RoomSettings, start?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomCreate(roomSettings, start, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomCreate']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1489,9 +1555,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomGet(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
async roomGet(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomGet(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1500,22 +1568,39 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomGetByName(roomName: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
async roomGetByName(roomName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomGetByName(roomName, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomGetByName']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Pause room
* @param {string} roomId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomPause(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomPause(roomId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomPause']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Recreate room
* @param {string} roomId
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomRecreate(roomId: string, start?: boolean, roomSettings?: RoomSettings, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomRecreate(roomId, start, roomSettings, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
async roomRecreate(roomId: string, roomSettings: RoomSettings, start?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomRecreate(roomId, roomSettings, start, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomRecreate']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1524,9 +1609,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomRemove(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async roomRemove(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomRemove(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomRemove']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1535,9 +1622,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomRestart(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async roomRestart(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomRestart(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomRestart']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1546,9 +1635,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomSettings(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomSettings>> {
async roomSettings(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomSettings>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomSettings(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomSettings']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1557,9 +1648,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomStart(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async roomStart(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomStart(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomStart']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1568,9 +1661,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomStats(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomStats>> {
async roomStats(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomStats>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomStats(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomStats']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1579,9 +1674,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomStop(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
async roomStop(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomStop(roomId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomStop']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
@@ -1590,9 +1687,11 @@ export const RoomsApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async roomsList(labels?: { [key: string]: string; }, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<RoomEntry>>> {
async roomsList(labels?: { [key: string]: string; }, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<RoomEntry>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.roomsList(labels, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RoomsApi.roomsList']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@@ -1610,19 +1709,19 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
exportAsDockerCompose(options?: any): AxiosPromise<void> {
exportAsDockerCompose(options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.exportAsDockerCompose(options).then((request) => request(axios, basePath));
},
/**
*
* @summary Create new room
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomCreate(start?: boolean, roomSettings?: RoomSettings, options?: any): AxiosPromise<RoomEntry> {
return localVarFp.roomCreate(start, roomSettings, options).then((request) => request(axios, basePath));
roomCreate(roomSettings: RoomSettings, start?: boolean, options?: RawAxiosRequestConfig): AxiosPromise<RoomEntry> {
return localVarFp.roomCreate(roomSettings, start, options).then((request) => request(axios, basePath));
},
/**
*
@@ -1631,7 +1730,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomGet(roomId: string, options?: any): AxiosPromise<RoomEntry> {
roomGet(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<RoomEntry> {
return localVarFp.roomGet(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1641,20 +1740,30 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomGetByName(roomName: string, options?: any): AxiosPromise<RoomEntry> {
roomGetByName(roomName: string, options?: RawAxiosRequestConfig): AxiosPromise<RoomEntry> {
return localVarFp.roomGetByName(roomName, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Pause room
* @param {string} roomId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomPause(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.roomPause(roomId, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Recreate room
* @param {string} roomId
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRecreate(roomId: string, start?: boolean, roomSettings?: RoomSettings, options?: any): AxiosPromise<RoomEntry> {
return localVarFp.roomRecreate(roomId, start, roomSettings, options).then((request) => request(axios, basePath));
roomRecreate(roomId: string, roomSettings: RoomSettings, start?: boolean, options?: RawAxiosRequestConfig): AxiosPromise<RoomEntry> {
return localVarFp.roomRecreate(roomId, roomSettings, start, options).then((request) => request(axios, basePath));
},
/**
*
@@ -1663,7 +1772,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRemove(roomId: string, options?: any): AxiosPromise<void> {
roomRemove(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.roomRemove(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1673,7 +1782,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomRestart(roomId: string, options?: any): AxiosPromise<void> {
roomRestart(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.roomRestart(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1683,7 +1792,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomSettings(roomId: string, options?: any): AxiosPromise<RoomSettings> {
roomSettings(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<RoomSettings> {
return localVarFp.roomSettings(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1693,7 +1802,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomStart(roomId: string, options?: any): AxiosPromise<void> {
roomStart(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.roomStart(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1703,7 +1812,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomStats(roomId: string, options?: any): AxiosPromise<RoomStats> {
roomStats(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<RoomStats> {
return localVarFp.roomStats(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1713,7 +1822,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomStop(roomId: string, options?: any): AxiosPromise<void> {
roomStop(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.roomStop(roomId, options).then((request) => request(axios, basePath));
},
/**
@@ -1723,7 +1832,7 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
roomsList(labels?: { [key: string]: string; }, options?: any): AxiosPromise<Array<RoomEntry>> {
roomsList(labels?: { [key: string]: string; }, options?: RawAxiosRequestConfig): AxiosPromise<Array<RoomEntry>> {
return localVarFp.roomsList(labels, options).then((request) => request(axios, basePath));
},
};
@@ -1743,21 +1852,21 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public exportAsDockerCompose(options?: AxiosRequestConfig) {
public exportAsDockerCompose(options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).exportAsDockerCompose(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Create new room
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomCreate(start?: boolean, roomSettings?: RoomSettings, options?: AxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomCreate(start, roomSettings, options).then((request) => request(this.axios, this.basePath));
public roomCreate(roomSettings: RoomSettings, start?: boolean, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomCreate(roomSettings, start, options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -1768,7 +1877,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomGet(roomId: string, options?: AxiosRequestConfig) {
public roomGet(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomGet(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1780,22 +1889,34 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomGetByName(roomName: string, options?: AxiosRequestConfig) {
public roomGetByName(roomName: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomGetByName(roomName, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Pause room
* @param {string} roomId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomPause(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomPause(roomId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Recreate room
* @param {string} roomId
* @param {RoomSettings} roomSettings
* @param {boolean} [start]
* @param {RoomSettings} [roomSettings]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomRecreate(roomId: string, start?: boolean, roomSettings?: RoomSettings, options?: AxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomRecreate(roomId, start, roomSettings, options).then((request) => request(this.axios, this.basePath));
public roomRecreate(roomId: string, roomSettings: RoomSettings, start?: boolean, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomRecreate(roomId, roomSettings, start, options).then((request) => request(this.axios, this.basePath));
}
/**
@@ -1806,7 +1927,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomRemove(roomId: string, options?: AxiosRequestConfig) {
public roomRemove(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomRemove(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1818,7 +1939,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomRestart(roomId: string, options?: AxiosRequestConfig) {
public roomRestart(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomRestart(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1830,7 +1951,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomSettings(roomId: string, options?: AxiosRequestConfig) {
public roomSettings(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomSettings(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1842,7 +1963,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomStart(roomId: string, options?: AxiosRequestConfig) {
public roomStart(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomStart(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1854,7 +1975,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomStats(roomId: string, options?: AxiosRequestConfig) {
public roomStats(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomStats(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1866,7 +1987,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomStop(roomId: string, options?: AxiosRequestConfig) {
public roomStop(roomId: string, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomStop(roomId, options).then((request) => request(this.axios, this.basePath));
}
@@ -1878,7 +1999,7 @@ export class RoomsApi extends BaseAPI {
* @throws {RequiredError}
* @memberof RoomsApi
*/
public roomsList(labels?: { [key: string]: string; }, options?: AxiosRequestConfig) {
public roomsList(labels?: { [key: string]: string; }, options?: RawAxiosRequestConfig) {
return RoomsApiFp(this.configuration).roomsList(labels, options).then((request) => request(this.axios, this.basePath));
}
}
+17 -3
View File
@@ -16,7 +16,7 @@
import type { Configuration } from './configuration';
// Some imports not used depending on template conditions
// @ts-ignore
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
export const BASE_PATH = "https://virtserver.swaggerhub.com/m1k1o/neko-rooms/1.0.0".replace(/\/+$/, "");
@@ -39,7 +39,7 @@ export const COLLECTION_FORMATS = {
*/
export interface RequestArgs {
url: string;
options: AxiosRequestConfig;
options: RawAxiosRequestConfig;
}
/**
@@ -53,7 +53,7 @@ export class BaseAPI {
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
if (configuration) {
this.configuration = configuration;
this.basePath = configuration.basePath || this.basePath;
this.basePath = configuration.basePath ?? basePath;
}
}
};
@@ -70,3 +70,17 @@ export class RequiredError extends Error {
this.name = "RequiredError"
}
}
interface ServerMap {
[key: string]: {
url: string,
description: string,
}[];
}
/**
*
* @export
*/
export const operationServerMap: ServerMap = {
}
+1 -1
View File
@@ -144,7 +144,7 @@ export const toPathString = function (url: URL) {
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url};
const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}
+15 -1
View File
@@ -19,6 +19,7 @@ export interface ConfigurationParameters {
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
serverIndex?: number;
baseOptions?: any;
formDataCtor?: new () => any;
}
@@ -58,6 +59,13 @@ export class Configuration {
* @memberof Configuration
*/
basePath?: string;
/**
* override server index
*
* @type {number}
* @memberof Configuration
*/
serverIndex?: number;
/**
* base options for axios calls
*
@@ -80,7 +88,13 @@ export class Configuration {
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.baseOptions = param.baseOptions;
this.serverIndex = param.serverIndex;
this.baseOptions = {
...param.baseOptions,
headers: {
...param.baseOptions?.headers,
},
};
this.formDataCtor = param.formDataCtor;
}
+12 -3
View File
@@ -1,7 +1,8 @@
<template>
<v-tooltip bottom v-if="tmpl" open-delay="300">
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" @click="Action" :color="tmpl.color" :disabled="disabled" :loading="loading" icon><v-icon>{{ tmpl.icon }}</v-icon></v-btn>
<v-btn v-bind="attrs" v-on="on" @click="Action" :color="tmpl.color" :disabled="disabled" :loading="loading"
icon><v-icon>{{ tmpl.icon }}</v-icon></v-btn>
</template>
<span>{{ tmpl.tooltip }}</span>
</v-tooltip>
@@ -38,6 +39,14 @@ export default class RoomActionBtn extends Vue {
color: 'warning',
icon: 'mdi-stop-circle-outline',
}
case 'pause':
return {
dispatch: 'ROOMS_PAUSE',
msg: 'Room paused!',
tooltip: 'Pause',
color: 'orange',
icon: 'mdi-pause-circle-outline',
}
case 'restart':
return {
dispatch: 'ROOMS_RESTART',
@@ -95,14 +104,14 @@ export default class RoomActionBtn extends Vue {
}
this.loading = true
try {
await this.$store.dispatch(this.tmpl.dispatch, this.roomId)
this.$swal({
title: this.tmpl.msg,
icon: 'success',
})
} catch(e) {
} catch (e) {
const response = (e as AxiosError).response
if (response) {
this.$swal({
+4 -4
View File
@@ -225,7 +225,7 @@
<h2> Environment variables </h2>
<v-btn @click="addEnv" icon color="green"><v-icon>mdi-plus</v-icon></v-btn>
</v-row>
<v-row align="center" v-for="({ key, val }, index) in envList" :key="index">
<v-row align="center" v-for="({ key, val }, index) in envList" :key="'env-'+index">
<v-col class="py-0">
<v-text-field
label="Key"
@@ -251,7 +251,7 @@
<h2> Mounts </h2>
<v-btn @click="addMount" icon color="green"><v-icon>mdi-plus</v-icon></v-btn>
</v-row>
<v-row align="center" class="mb-2" v-for="({ type, host_path, container_path }, index) in data.mounts" :key="index">
<v-row align="center" class="mb-2" v-for="({ type, host_path, container_path }, index) in data.mounts" :key="'mount-'+index">
<v-col class="py-0" cols="2">
<v-select
label="Type"
@@ -726,13 +726,13 @@ export default class RoomsCreate extends Vue {
} else {
mounts[index] = data
}
Vue.set(this.data, 'mounts', mounts)
Vue.set(this.data, 'mounts', [ ...mounts])
}
delMount(index: number) {
const mounts = this.data.mounts || []
mounts.splice(index, 1)
Vue.set(this.data, 'mounts', mounts)
Vue.set(this.data, 'mounts', [ ...mounts])
}
async CreateAndStart() {
+10 -12
View File
@@ -1,23 +1,19 @@
<template>
<div>
<v-data-table
:headers="headers"
:items="rooms"
class="elevation-1"
:loading="loading"
loading-text="Loading... Please wait"
hide-default-footer
>
<v-data-table :headers="headers" :items="rooms" class="elevation-1" :loading="loading"
loading-text="Loading... Please wait" hide-default-footer>
<template v-slot:[`item.url`]="{ item }">
<v-tooltip bottom open-delay="300">
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" @click="roomId = item.id; dialog = true" color="blue" small class="mr-2"> <v-icon small>mdi-information-outline</v-icon></v-btn>
<v-btn v-bind="attrs" v-on="on" @click="roomId = item.id; dialog = true" color="blue" small class="mr-2">
<v-icon small>mdi-information-outline</v-icon></v-btn>
</template>
<span>View more information</span>
</v-tooltip>
<v-tooltip bottom open-delay="300">
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" :disabled="!item.running" :href="item.url" target="_blank" small> <v-icon small>mdi-open-in-new</v-icon></v-btn>
<v-btn v-bind="attrs" v-on="on" :disabled="!item.running" :href="item.url" target="_blank" small> <v-icon
small>mdi-open-in-new</v-icon></v-btn>
</template>
<span>Link to deployment</span>
</v-tooltip>
@@ -37,14 +33,16 @@
<i v-else>uses mux</i>
</template>
<template v-slot:[`item.status`]="{ item }">
<v-chip :color="item.running ? (item.status.includes('unhealthy') ? 'warning' : 'green') : 'red'" dark small> {{ item.status }} </v-chip>
<v-chip :color="item.running ? (item.status.includes('unhealthy') ? 'warning' : 'green') : 'red'" dark small> {{
item.status }} </v-chip>
</template>
<template v-slot:[`item.created`]="{ item }">
{{ item.created | timeago }}
</template>
<template v-slot:[`item.actions`]="{ item }">
<RoomActionBtn action="start" :roomId="item.id" :disabled="item.running" />
<RoomActionBtn action="stop" :roomId="item.id" :disabled="!item.running" />
<RoomActionBtn action="stop" :roomId="item.id" :disabled="!item.running && !item.paused" />
<RoomActionBtn action="pause" :roomId="item.id" :disabled="!item.running" />
<RoomActionBtn action="restart" :roomId="item.id" :disabled="!item.running" />
</template>
<template v-slot:[`item.destroy`]="{ item }">
+23 -6
View File
@@ -84,17 +84,17 @@ export default new Vuex.Store({
const res = await roomsApi.roomsList()
commit('ROOMS_SET', res.data);
},
async ROOMS_CREATE({ commit }: ActionContext<State, State>, roomSettings: RoomSettings): Promise<RoomEntry> {
const res = await roomsApi.roomCreate(false, roomSettings)
async ROOMS_CREATE({ commit }: ActionContext<State, State>, roomSettings: RoomSettings): Promise<RoomEntry> {
const res = await roomsApi.roomCreate(roomSettings, false)
commit('ROOMS_ADD', res.data);
return res.data
},
async ROOMS_CREATE_AND_START({ commit }: ActionContext<State, State>, roomSettings: RoomSettings): Promise<RoomEntry> {
const res = await roomsApi.roomCreate(true, roomSettings)
async ROOMS_CREATE_AND_START({ commit }: ActionContext<State, State>, roomSettings: RoomSettings): Promise<RoomEntry> {
const res = await roomsApi.roomCreate(roomSettings, true)
commit('ROOMS_ADD', res.data);
return res.data
},
async ROOMS_GET({ commit }: ActionContext<State, State>, roomId: string) {
async ROOMS_GET({ commit }: ActionContext<State, State>, roomId: string) {
const res = await roomsApi.roomGet(roomId)
commit('ROOMS_PUT', res.data);
return res.data
@@ -116,6 +116,7 @@ export default new Vuex.Store({
commit('ROOMS_PUT', {
id: roomId,
running: true,
paused: false,
status: 'Up',
});
},
@@ -124,14 +125,24 @@ export default new Vuex.Store({
commit('ROOMS_PUT', {
id: roomId,
running: false,
paused: false,
status: 'Exited',
});
},
async ROOMS_PAUSE({ commit }: ActionContext<State, State>, roomId: string) {
await roomsApi.roomPause(roomId)
commit('ROOMS_PUT', {
id: roomId,
running: false,
paused: true,
status: 'Paused',
});
},
async ROOMS_RESTART(_: ActionContext<State, State>, roomId: string) {
await roomsApi.roomRestart(roomId)
},
async ROOMS_RECREATE({ commit }: ActionContext<State, State>, roomId: string) {
const res = await roomsApi.roomRecreate(roomId)
const res = await roomsApi.roomRecreate(roomId, {} as RoomSettings)
commit('ROOMS_DEL', roomId)
commit('ROOMS_PUT', res.data)
return res.data
@@ -153,6 +164,12 @@ export default new Vuex.Store({
const res = await defaultApi.pullStop()
return res.data
},
async EVENTS_SSE(): Promise<EventSource> {
return new EventSource(configuration.basePath + '/api/events?sse', {
withCredentials: true,
})
},
},
modules: {
}
+13 -1
View File
@@ -189,7 +189,19 @@ export const state = {
url: 'https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi',
} as BrowserPolicyExtension,
[BrowserPolicyTypeEnum.chromium]: {
id: 'cjpalhdlnbpafiamejdnhcphjbkeiagm',
id: 'ddkjiahejlhfcafbddmgiahcphecmpfh', // uBlock Origin Lite
} as BrowserPolicyExtension,
},
},
{
text: 'AdGuard AdBlocker',
value: {
[BrowserPolicyTypeEnum.firefox]: {
id: 'adguardadblocker@adguard.com',
url: 'https://addons.mozilla.org/firefox/downloads/latest/adguard-adblocker/latest.xpi',
} as BrowserPolicyExtension,
[BrowserPolicyTypeEnum.chromium]: {
id: 'apjcbfpjihpedihablmalmbbhjpklbdf',
} as BrowserPolicyExtension,
},
},
+63 -4
View File
@@ -83,9 +83,10 @@ export default class Home extends Vue {
public dialog = false
public interval!: number
public autoRefresh = 10
public autoRefresh = 0
public autoRefreshItems = [
{ text: 'Off', value: 0 },
{ text: 'Off', value: -1 },
{ text: 'Live', value: 0 },
{ text: '5s', value: 5 },
{ text: '10s', value: 10 },
{ text: '30s', value: 30 },
@@ -127,8 +128,11 @@ export default class Home extends Vue {
window.clearInterval(this.interval)
}
if (this.autoRefresh) {
if (this.autoRefresh > 0) {
this.sseClose()
this.interval = window.setInterval(this.LoadRooms, this.autoRefresh * 1000)
} else if (this.autoRefresh == 0) {
this.sseStart()
}
}
@@ -148,7 +152,62 @@ export default class Home extends Vue {
}
beforeDestroy() {
this.autoRefresh = 0
this.autoRefresh = -1
}
sse: EventSource | null = null
sseIsOpen = false
sseReconnBackoff = 1000
sseReconnRetries = 0
async sseStart(reconnect = false) {
if (!reconnect) {
this.sseIsOpen = true
}
if (this.sse) {
this.sse.close()
}
this.sse = await this.$store.dispatch('EVENTS_SSE') as EventSource
this.sseReconnRetries++
this.sse.addEventListener('rooms', (e) => {
console.log('SSE rooms', e)
this.LoadRooms()
})
this.sse.addEventListener('error', (e: Event) => {
console.log('SSE error', e)
// if is closed, don't try to reconnect
if (!this.sseIsOpen) return
const wait = this.sseReconnBackoff + 100 * this.sseReconnRetries
// try to reconnect
setTimeout(() => {
this.sseStart(true)
}, wait)
console.log('SSE error, trying to reconnect in ' + wait + 'ms')
})
this.sse.addEventListener('message', (e: Event) => {
console.log('SSE message', e)
})
this.sse.addEventListener('open', (e: Event) => {
console.log('SSE opened', e)
})
}
sseClose() {
if (this.sse) {
this.sse.close()
}
this.sse = null
this.sseIsOpen = false
}
}
</script>
+2
View File
@@ -11,7 +11,9 @@ module.exports = defineConfig({
proxy: process.env.API_PROXY ? {
'^/api': {
target: process.env.API_PROXY,
timeout: 0, // because of SSE
},
} : undefined,
compress: false, // because of SSE
}
})
+19
View File
@@ -0,0 +1,19 @@
# Community Installation Scripts
This directory contains community-maintained installation scripts for various operating systems and configurations. While these scripts are not officially supported, they can be helpful for setting up neko-rooms on different platforms.
## Available Scripts
### AWS Linux
For Amazon Linux 2 or higher:
```bash
wget -O neko-rooms-aws.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/refs/heads/master/community/scripts/aws-linux.sh
sudo bash neko-rooms-aws.sh
```
### Ubuntu/Debian with Nginx
For Ubuntu 20.04 or higher, or Debian 10 or higher:
```bash
wget -O neko-rooms.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/refs/heads/master/community/scripts/ubuntu-debian.sh
sudo bash neko-rooms.sh
```
+801
View File
@@ -0,0 +1,801 @@
#!/bin/bash
set -e
# Colors for output
GREEN='\033[0;32m'
AQUA='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Variables to track the installation progress
CREATE_DOCKER_COMPOSE=false
CLONE_NEKO_ROOMS=false
CREATE_NGINX_CONFIG=false
CREATE_HTPASSWD=false
OBTAIN_CERTIFICATE=false
CREATE_CRON_JOB=false
CREATE_MANAGE_SCRIPT=false
# Function to roll back the installation on error
rollback() {
echo -e "${RED}\nAn error occurred. Rolling back partial installation...${NC}"
# 1. Remove Docker containers/images if the compose file was created
if [ "$CREATE_DOCKER_COMPOSE" == true ]; then
echo -e "${YELLOW}Removing Docker containers and images...${NC}"
cd /opt/neko-rooms || true
docker-compose down || true
docker rmi m1k1o/neko-rooms:latest -f || true
rm -f docker-compose.yml
cd ~ || true
fi
# 2. Remove cloned repository
if [ "$CLONE_NEKO_ROOMS" == true ]; then
echo -e "${YELLOW}Removing cloned neko-rooms repo...${NC}"
rm -rf "/opt/neko-rooms" || true
fi
# 3. Remove NGINX configuration
if [ "$CREATE_NGINX_CONFIG" == true ]; then
echo -e "${YELLOW}Removing NGINX configuration...${NC}"
rm -f "/etc/nginx/sites-available/${DOMAIN}-neko-rooms.conf"
rm -f "/etc/nginx/sites-enabled/${DOMAIN}-neko-rooms.conf"
systemctl reload nginx || true
fi
# 4. Remove .htpasswd file
if [ "$CREATE_HTPASSWD" == true ]; then
echo -e "${YELLOW}Removing .htpasswd file...${NC}"
rm -f "/etc/nginx/.htpasswd"
fi
# 5. Remove Certbot certificates and configuration
if [ "$OBTAIN_CERTIFICATE" == true ]; then
echo -e "${YELLOW}Removing generated certificates...${NC}"
certbot delete --cert-name "$DOMAIN" -n || true
rm -f "/etc/letsencrypt/renewal/${DOMAIN}.conf" || true
fi
# 6. Remove the cron job
if [ "$CREATE_CRON_JOB" == true ]; then
echo -e "${YELLOW}Removing Certbot cron job...${NC}"
rm -f "/etc/cron.d/certbot-renew"
systemctl restart crond || true
fi
# 7. Remove the manage_htpasswd script
if [ "$CREATE_MANAGE_SCRIPT" == true ]; then
echo -e "${YELLOW}Removing manage_htpasswd.sh...${NC}"
rm -f "/usr/local/bin/manage_htpasswd.sh"
fi
echo -e "${GREEN}================================================${NC}"
echo -e "${RED}Rollback complete. Exiting.${NC}"
echo -e "${GREEN}================================================${NC}"
exit 1
}
# Function to get all server IPs
get_server_ips() {
local ip_list
local fallback_ip_service="checkip.amazonaws.com"
ip_list=$(hostname -I | awk '{for (i=1; i<=NF; i++) if ($i !~ /^172\./ && $i !~ /^10\./ && $i !~ /^192\.168\./) print $i}' | tr '\n' ', ' | sed 's/, $//')
# If the hostname command fails, use the following methods
if [ -z "$ip_list" ]; then
ip_list=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null)
if [ $? -ne 0 ]; then
echo "dig command failed, falling back to $fallback_ip_service" >&2
fi
fi
if [ -z "$ip_list" ]; then
ip_list=$(curl -s $fallback_ip_service)
fi
echo "$ip_list"
}
# Get all server IPs
SERVER_IPS=$(get_server_ips)
# Introduction message
echo -e "${GREEN}==================================================${NC}"
echo -e "${GREEN}Neko Rooms Installer${NC} ${AQUA}by H1ghSyst3m${NC}"
echo -e "This scipt uses nginx as reverse proxy, certbot and docker to install Neko Rooms."
echo -e "${GREEN}==================================================${NC}"
echo -e "${YELLOW}This script will:${NC}"
echo -e " 1. Install Docker, Docker Compose, NGINX, and Certbot."
echo -e " 2. Deploy Neko Rooms using Docker Compose."
echo -e " 3. Configure NGINX as a reverse proxy."
echo -e " 4. Obtain and configure an SSL certificate."
echo -e " 5. Add a cron job for automatic SSL renewal."
echo -e "${GREEN}==================================================${NC}"
echo -e "${YELLOW}Please ensure the following before proceeding:${NC}"
echo -e " - ${GREEN}OS:${NC}"
echo -e " - Kernel version 2 or higher."
echo -e " - Debian 9 or higher."
echo -e " - Amazon Linux"
echo -e " - ${GREEN}Hardware:${NC}"
echo -e " - Memory at least 2GB."
echo -e " - CPU at least 4 cores."
echo -e " - Disk at least 8GB."
echo -e " - ${GREEN}Network:${NC}"
echo -e " - Public IP: $SERVER_IPS"
echo -e " - Free TCP ports: 80 and 443."
echo -e " - Free UDP port range: 59000-59100."
echo -e " - Domain name pointing to your IP: $SERVER_IPS"
echo -e " - Run this script as superuser."
echo -e "${GREEN}==================================================${NC}\n"
# Detect shell and root privileges
if [ -n "$BASH_VERSION" ]; then
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
echo 'This installer requires Bash version 4 or higher.' >&2
exit 1
fi
else
echo 'This installer needs to be run with "bash", not "sh" or another shell.' >&2
exit 1
fi
# Check if the script is run as root
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}Please run as root or use sudo.${NC}"
exit 1
fi
# Function to ask the user to continue
ask_to_continue() {
while true; do
echo -e "${YELLOW}Do you want to continue? (yes/no)${NC}"
read -r response
case "$response" in
[Yy]* ) break ;;
[Nn]* )
echo -e "${RED}Installation aborted by user.${NC}"
exit 1 ;;
* )
echo -e "${RED}Invalid input. Please answer yes or no.${NC}" ;;
esac
done
}
# Function to detect OS and Kernel
detect_os_and_kernel() {
# Detect OS
if [ -f /etc/os-release ]; then
source /etc/os-release
if [[ "$ID" == "amzn" ]]; then
OS_VERSION="$(echo $VERSION_ID | cut -d '.' -f1)"
if [[ "$OS_VERSION" -lt 2 ]]; then
echo "Amazon Linux 2 or higher is required to use this installer." >&2
echo "This version of Amazon Linux is too old and unsupported." >&2
exit 1
fi
else
echo "This installer is designed for Amazon Linux." >&2
echo "Current OS: $PRETTY_NAME" >&2
exit 1
fi
else
echo "Could not determine the operating system." >&2
exit 1
fi
# Detect Kernel
if [[ "$(uname -r | cut -d "." -f 1)" -lt 4 ]]; then
echo "The system is running an old kernel, which is incompatible with this installer." >&2
exit 1
fi
}
# Function to check and install required packages
install_required_packages() {
echo -e "${YELLOW}Checking and installing required packages: bind-utils and git...${NC}"
# Update package list and install required packages
yum update -y >/dev/null 2>&1
yum install -y bind-utils git >/dev/null 2>&1
# Verify installation of `dig` (from bind-utils)
if ! command -v dig &>/dev/null; then
echo -e "${RED}Failed to install bind-utils. Please check your network or package manager settings.${NC}"
exit 1
fi
# Verify installation of `git`
if ! command -v git &>/dev/null; then
echo -e "${RED}Failed to install git. Please check your network or package manager settings.${NC}"
exit 1
fi
echo -e "${GREEN}Required packages installed successfully.${NC}"
}
# Function to validate user input
validate_input() {
local prompt="$1"
local var_name="$2"
local default="$3"
while true; do
echo -e "${YELLOW}${prompt}${NC} ${default:+[Default: $default]}"
read input
input="${input:-$default}"
if [ -z "$input" ]; then
echo -e "${RED}Input cannot be empty. Please try again.${NC}"
else
eval "$var_name=\"$input\""
break
fi
done
}
# Function to validate email
validate_email() {
local email="$1"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Function to validate domain
validate_domain() {
local domain="$1"
if [[ "$domain" =~ ^(([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)\.)+[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Function to validate and get domain
validate_and_get_domain() {
while true; do
validate_input "Enter your domain name (e.g., watch.example.com, example.com):" DOMAIN
if validate_domain "$DOMAIN"; then
# Get the A record of the domain
DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n 1)
# Convert SERVER_IPS into a line-separated list for better matching
SERVER_IPS_LIST=$(echo "$SERVER_IPS" | tr ',' '\n' | tr -d ' ')
# Check if the domain IP matches any server IP
if echo "$SERVER_IPS_LIST" | grep -q "^$DOMAIN_IP$"; then
echo -e "${GREEN}Domain $DOMAIN is correctly pointing to the server's IP: $DOMAIN_IP.${NC}"
break
else
echo -e "${RED}Domain $DOMAIN does not point to any of the server's IPs.${NC}"
echo -e "${YELLOW}Current IP of $DOMAIN: ${DOMAIN_IP:-Not Found}${NC}"
echo -e "${YELLOW}Your Server IPs: $SERVER_IPS${NC}"
echo -e "${YELLOW}Please update the domain's A record to point to one of the server's IPs and try again.${NC}"
echo -e ""
# Ask the user if they want to continue with the domain anyway
while true; do
echo -e "${YELLOW}Do you want to continue with this domain anyway? (yes/no)${NC}"
read -r response
case "$response" in
[Yy]* ) echo -e "${YELLOW}Continuing with domain $DOMAIN despite the IP mismatch.${NC}"; break 2 ;;
[Nn]* ) echo -e "${RED}Please update the domain's A record and try again.${NC}"; break ;;
* ) echo -e "${RED}Invalid input. Please answer yes or no.${NC}" ;;
esac
done
fi
else
echo -e "${RED}Invalid domain name. Please enter a valid domain.${NC}"
fi
done
}
# Function to validate and get email
validate_and_get_email() {
while true; do
validate_input "Enter your email for SSL certificate:" EMAIL
if validate_email "$EMAIL"; then
break
else
echo -e "${RED}Invalid email address. Please enter a valid email.${NC}"
fi
done
}
# Function to confirm passwords
confirm_password() {
local prompt="$1"
local var_name="$2"
while true; do
echo -e "${YELLOW}${prompt}:${NC}"
read -s password1
echo -e "${YELLOW}Confirm password:${NC}"
read -s password2
if [ "$password1" != "$password2" ]; then
echo -e "${RED}Passwords do not match. Please try again.${NC}"
else
eval "$var_name=\"$password1\""
break
fi
done
}
# Function to validate and check Room port range
check_ports() {
local port_range="$1"
IFS='-' read -r start_port end_port <<<"$port_range"
if [[ ! "$start_port" =~ ^[0-9]+$ ]] || [[ ! "$end_port" =~ ^[0-9]+$ ]] || ((start_port > end_port)); then
echo -e "${RED}Invalid port range: $port_range. Please use the format START-END (e.g., 59000-59100).${NC}"
return 1
fi
for ((port = start_port; port <= end_port; port++)); do
if ss -lnu | grep -q ":$port "; then
echo -e "${RED}Port $port is already in use. Please choose a different range.${NC}"
return 1
fi
done
echo -e "${GREEN}Ports $port_range are free.${NC}"
return 0
}
# Function to validate a single port
validate_port() {
local port="$1"
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
return 0
else
return 1
fi
}
# Check if a port is available
check_port_availability() {
local port="$1"
if ss -ln | grep -q ":$port "; then
return 1
else
return 0
fi
}
# Function to validate and check Room port range
validate_room_port_range() {
while true; do
validate_input "Enter the Room port range [Default: 59000-59100]:" ROOM_PORT_RANGE "59000-59100"
if check_ports "$ROOM_PORT_RANGE"; then
break
else
echo -e "${YELLOW}Please enter a valid and free port range.${NC}"
fi
done
}
# Function to validate and check Docker port
validate_docker_port() {
while true; do
validate_input "Enter the Docker port for Neko Rooms [Default: 8080]:" DOCKER_PORT "8080"
if validate_port "$DOCKER_PORT" && check_port_availability "$DOCKER_PORT"; then
echo -e "${GREEN}Port $DOCKER_PORT is available.${NC}"
break
else
echo -e "${RED}Port $DOCKER_PORT is either invalid or already in use. Please choose another port.${NC}"
fi
done
}
# Summarize Configuration
summary() {
echo -e "\n${GREEN}Configuration Summary:${NC}"
echo -e "${YELLOW}Domain:${NC} $DOMAIN"
echo -e "${YELLOW}Email:${NC} $EMAIL"
echo -e "${YELLOW}Timezone:${NC} $TIMEZONE"
echo -e "${YELLOW}Room Port Range:${NC} $ROOM_PORT_RANGE"
echo -e "${YELLOW}Docker Port:${NC} $DOCKER_PORT"
echo -e "${YELLOW}Rooms Path Prefix:${NC} $PATH_PREFIX"
echo -e "${YELLOW}Pointed IP:${NC} $DOMAIN_IP"
echo -e "\n${GREEN}Please review the above settings carefully.${NC}"
}
ask_to_continue
detect_os_and_kernel
echo ""
install_required_packages
echo ""
echo ""
validate_and_get_domain
echo ""
validate_and_get_email
echo ""
validate_input "Enter admin username for authentication:" ADMIN_USER
echo ""
confirm_password "Enter admin password for authentication" ADMIN_PASSWORD
echo ""
validate_input "Enter your timezone (e.g., Europe/Vienna) [Default: UTC]:" TIMEZONE "UTC"
echo ""
validate_room_port_range
echo ""
validate_docker_port
echo ""
echo -e "${YELLOW}Choose a path prefix for accessing Neko Rooms.${NC}"
echo -e "${YELLOW}Example: If you choose 'room', rooms will be accessible at:${NC} ${GREEN}https://${DOMAIN}/room/<room-id>${NC}"
validate_input "Enter your path prefix (e.g., room, home, browser) [Default: room]:" PATH_PREFIX "room"
summary
echo ""
ask_to_continue
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
install_packages() {
local packages=$@
echo -e "${YELLOW}Installing required packages: $packages...${NC}"
# Start installation in the background
(yum update -y && yum install -y $packages) &
# Get the process ID and show the spinner
show_spinner $!
}
# Precheck for Docker
check_docker() {
if command_exists docker; then
echo -e "${GREEN}Docker is already installed.${NC}"
else
echo -e "${YELLOW}Docker is not installed. Installing Docker...${NC}"
curl -sSL https://get.docker.com/ | CHANNEL=stable bash
systemctl enable docker
systemctl start docker
fi
}
# Precheck for Docker Compose
check_docker_compose() {
if command_exists docker-compose; then
echo -e "${GREEN}Docker Compose is already installed.${NC}"
else
echo -e "${YELLOW}Docker Compose is not installed. Installing Docker Compose...${NC}"
DOCKER_COMPOSE_VERSION="1.29.2"
curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
fi
}
# Precheck for Crontab
check_crontab() {
if command -v crontab >/dev/null 2>&1; then
echo -e "${GREEN}Crontab is already installed.${NC}"
else
echo -e "${YELLOW}Crontab is not installed. Installing...${NC}"
yum install -y cronie
systemctl enable crond
systemctl start crond
fi
}
# Precheck for NGINX
check_nginx() {
if command_exists nginx; then
echo -e "${GREEN}NGINX is already installed.${NC}"
else
echo -e "${YELLOW}NGINX is not installed. Installing NGINX...${NC}"
yum install -y nginx
systemctl enable nginx
systemctl start nginx
fi
}
# Precheck and Installation for Docker, Docker Compose, crontab and NGINX
echo -e "${AQUA}\n==================================================${NC}"
check_docker
echo -e "${AQUA}\n==================================================${NC}"
check_docker_compose
echo -e "${AQUA}\n==================================================${NC}"
check_crontab
echo -e "${AQUA}\n==================================================${NC}"
check_nginx
echo -e "${AQUA}\n==================================================${NC}"
# Install other dependencies
echo -e "${YELLOW}Installing required packages...${NC}"
yum install -y certbot python3-certbot-nginx httpd-tools
echo -e "${AQUA}\n==================================================${NC}"
# Clone Neko Rooms repository
echo -e "${YELLOW}Cloning Neko Rooms repository...${NC}"
git clone https://github.com/m1k1o/neko-rooms.git /opt/neko-rooms || echo -e "${GREEN}Neko Rooms repository already exists. Skipping...${NC}"
cd /opt/neko-rooms
CLONE_NEKO_ROOMS=true
echo -e "${AQUA}\n==================================================${NC}"
# Modify docker-compose.yml
echo -e "${YELLOW}Configuring Neko Rooms${NC} ${AQUA}docker-compose.yml${NC}${YELLOW}...${NC}"
cat <<EOF > docker-compose.yml
version: "3.5"
networks:
default:
attachable: true
name: "neko-rooms-net"
services:
neko-rooms:
image: "m1k1o/neko-rooms:latest"
restart: "unless-stopped"
environment:
- "TZ=${TIMEZONE}"
- "NEKO_ROOMS_MUX=true"
- "NEKO_ROOMS_EPR=${ROOM_PORT_RANGE}"
- "NEKO_ROOMS_NAT1TO1=${DOMAIN_IP}"
- "NEKO_ROOMS_INSTANCE_URL=https://${DOMAIN}/"
- "NEKO_ROOMS_INSTANCE_NETWORK=neko-rooms-net"
- "NEKO_ROOMS_TRAEFIK_ENABLED=false"
- "NEKO_ROOMS_PATH_PREFIX=/${PATH_PREFIX}/"
- "NEKO_ROOMS_STORAGE_ENABLED=true"
- "NEKO_ROOMS_STORAGE_INTERNAL=/data"
- "NEKO_ROOMS_STORAGE_EXTERNAL=/opt/neko-rooms/data"
ports:
- "127.0.0.1:${DOCKER_PORT}:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "/opt/neko-rooms/data:/data"
EOF
CREATE_DOCKER_COMPOSE=true
echo -e "${GREEN} Neko Rooms configuration updated.${NC}"
echo -e "${AQUA}\n==================================================${NC}"
# Start Neko Rooms
echo -e "${GREEN}Starting Neko Rooms service...${NC}"
docker-compose up -d
echo -e "${AQUA}\n==================================================${NC}"
# Configure NGINX without SSL
echo -e "${YELLOW}Configuring NGINX without SSL...${NC}"
mkdir -p /etc/nginx/conf.d
cat <<EOF > /etc/nginx/conf.d/${DOMAIN}-neko-rooms.conf
server {
listen 80;
server_name ${DOMAIN};
location / {
proxy_pass http://127.0.0.1:${DOCKER_PORT};
proxy_set_header Host \$host;
proxy_http_version 1.1;
# WebSocket headers
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
# Forwarding headers for reverse proxy
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
proxy_buffering off;
proxy_read_timeout 900s;
}
# Admin Panel Restricted Access
location ~ ^/$ {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:${DOCKER_PORT};
}
}
EOF
# Amazon Linux uses conf.d directory, no need for symlinking
CREATE_NGINX_CONFIG=true
systemctl reload nginx
echo -e "${GREEN}NGINX configuration updated and reloaded.${NC}"
echo -e "${AQUA}\n==================================================${NC}"
# Create a password file for admin access
echo -e "${GREEN}Creating admin authentication...${NC}"
htpasswd -cb /etc/nginx/.htpasswd ${ADMIN_USER} ${ADMIN_PASSWORD}
CREATE_HTPASSWD=true
echo -e "${AQUA}\n==================================================${NC}"
# Obtain SSL certificate and configure NGINX for SSL
echo -e "${GREEN}Obtaining SSL certificate and updating NGINX for SSL...${NC}"
certbot --nginx -d ${DOMAIN} --non-interactive --agree-tos -m ${EMAIL}
OBTAIN_CERTIFICATE=true
systemctl reload nginx
echo -e "${AQUA}\n==================================================${NC}"
# Create a separate crontab file for Certbot renewal
echo -e "${GREEN}Creating a separate crontab file for Certbot auto-renewal...${NC}"
cat <<EOF > /etc/cron.d/certbot-renew
# Cron job for Certbot auto-renewal
0 3 * * 0 root certbot renew --quiet && systemctl reload nginx
EOF
CREATE_CRON_JOB=true
# Set appropriate permissions
chmod 644 /etc/cron.d/certbot-renew
# Reload cron to apply changes
echo -e "${GREEN}Reloading cron daemon to apply the new cron job...${NC}"
systemctl restart crond
# Verify the cron job
echo -e "${GREEN}Cron job file created at /etc/cron.d/certbot-renew:${NC}"
cat /etc/cron.d/certbot-renew
echo -e "${AQUA}\n==================================================${NC}"
# Generate a script to manage admin users
echo -e "${GREEN}Generating script to manage admin users...${NC}"
cat <<'EOF' > /usr/local/bin/manage_htpasswd.sh
#!/bin/bash
HTPASSWD_FILE="/etc/nginx/.htpasswd"
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Check if htpasswd exists
if ! command -v htpasswd &>/dev/null; then
echo -e "${RED}Error: htpasswd command not found. Please install httpd-tools.${NC}"
exit 1
fi
# Function to display usage
usage() {
echo -e "${YELLOW}Usage:${NC} $0 {add|remove|list|change-password} [username]"
echo -e "${YELLOW}Commands:${NC}"
echo -e " ${GREEN}add${NC} Add a new user or update an existing user's password"
echo -e " ${GREEN}remove${NC} Remove a user"
echo -e " ${GREEN}list${NC} List all users"
echo -e " ${GREEN}change-password${NC} Change the password of an existing user"
exit 1
}
# Ensure the .htpasswd file exists
if [ ! -f "$HTPASSWD_FILE" ]; then
echo -e "${YELLOW}The .htpasswd file does not exist. Creating a new one...${NC}"
touch "$HTPASSWD_FILE"
fi
# Function to prompt for a password
prompt_password() {
while true; do
echo -e "${YELLOW}Enter password:${NC}"
read -rsp "Password: " password1
echo
echo -e "${YELLOW}Confirm password:${NC}"
read -rsp "Confirm Password: " password2
echo
if [ "$password1" != "$password2" ]; then
echo -e "${RED}Passwords do not match. Please try again.${NC}"
elif [ -z "$password1" ]; then
echo -e "${RED}Password cannot be empty. Please try again.${NC}"
else
echo "$password1"
return 0
fi
done
}
# Add or update a user
add_user() {
local username="$1"
echo -e "${YELLOW}Adding or updating user '$username'.${NC}"
sudo htpasswd "$HTPASSWD_FILE" "$username"
if [ $? -eq 0 ]; then
echo -e "${GREEN}User '$username' has been added or updated successfully.${NC}"
else
echo -e "${RED}Failed to add or update user '$username'. Please check your inputs.${NC}"
exit 1
fi
}
# Change user password
change_password() {
local username="$1"
if ! grep -q "^$username:" "$HTPASSWD_FILE"; then
echo -e "${RED}Error: User '$username' does not exist.${NC}"
exit 1
fi
echo -e "${YELLOW}Changing password for user '$username'.${NC}"
sudo htpasswd "$HTPASSWD_FILE" "$username"
if [ $? -eq 0 ]; then
echo -e "${GREEN}Password for user '$username' has been updated successfully.${NC}"
else
echo -e "${RED}Failed to update password for user '$username'. Please check your inputs.${NC}"
exit 1
fi
}
# Remove a user
remove_user() {
local username="$1"
if ! grep -q "^$username:" "$HTPASSWD_FILE"; then
echo -e "${RED}Error: User '$username' does not exist.${NC}"
exit 1
fi
sed -i "/^$username:/d" "$HTPASSWD_FILE"
echo -e "${GREEN}User '$username' has been removed.${NC}"
}
# List all users
list_users() {
if [ ! -s "$HTPASSWD_FILE" ]; then
echo -e "${YELLOW}No users found in the .htpasswd file.${NC}"
else
echo -e "${GREEN}Users in the .htpasswd file:${NC}"
cut -d: -f1 "$HTPASSWD_FILE"
fi
}
# Main logic
case "$1" in
add)
if [ -z "$2" ]; then
echo -e "${RED}Error: Username is required for the 'add' command.${NC}"
usage
fi
add_user "$2"
;;
remove)
if [ -z "$2" ]; then
echo -e "${RED}Error: Username is required for the 'remove' command.${NC}"
usage
fi
remove_user "$2"
;;
list)
list_users
;;
change-password)
if [ -z "$2" ]; then
echo -e "${RED}Error: Username is required for the 'change-password' command.${NC}"
usage
fi
change_password "$2"
;;
*)
usage
;;
esac
EOF
CREATE_MANAGE_SCRIPT=true
# Make the script executable
chmod +x /usr/local/bin/manage_htpasswd.sh
echo -e "${GREEN}Script generated: /usr/local/bin/manage_htpasswd.sh${NC}"
echo -e "${YELLOW}You can use this script to manage admin panel users:${NC}"
echo -e "${YELLOW} - Add user: /usr/local/bin/manage_htpasswd.sh add <username>${NC}"
echo -e "${YELLOW} - Remove user: /usr/local/bin/manage_htpasswd.sh remove <username>${NC}"
echo -e "${YELLOW} - Change password: /usr/local/bin/manage_htpasswd.sh change-password <username>${NC}"
echo -e "${YELLOW} - List users: /usr/local/bin/manage_htpasswd.sh list${NC}"
echo -e "${AQUA}\n==================================================${NC}"
# Final message
echo -e "${GREEN}Installation complete! Access your Neko Rooms at https://${DOMAIN}${NC}"
+823
View File
@@ -0,0 +1,823 @@
#!/bin/bash
set -e
# Colors for output
GREEN='\033[0;32m'
AQUA='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Variables to track the installation progress
CREATE_DOCKER_COMPOSE=false
CLONE_NEKO_ROOMS=false
CREATE_NGINX_CONFIG=false
CREATE_HTPASSWD=false
OBTAIN_CERTIFICATE=false
CREATE_CRON_JOB=false
CREATE_MANAGE_SCRIPT=false
# Function to roll back the installation on error
rollback() {
echo -e "${RED}\nAn error occurred. Rolling back partial installation...${NC}"
# 1. Remove Docker containers/images if the compose file was created
if [ "$CREATE_DOCKER_COMPOSE" == true ]; then
echo -e "${YELLOW}Removing Docker containers and images...${NC}"
cd /opt/neko-rooms || true
docker-compose down || true
docker rmi m1k1o/neko-rooms:latest -f || true
rm -f docker-compose.yml
cd ~ || true
fi
# 2. Remove cloned repository
if [ "$CLONE_NEKO_ROOMS" == true ]; then
echo -e "${YELLOW}Removing cloned neko-rooms repo...${NC}"
rm -rf "/opt/neko-rooms" || true
fi
# 3. Remove NGINX configuration
if [ "$CREATE_NGINX_CONFIG" == true ]; then
echo -e "${YELLOW}Removing NGINX configuration...${NC}"
rm -f "/etc/nginx/sites-available/${DOMAIN}-neko-rooms.conf"
rm -f "/etc/nginx/sites-enabled/${DOMAIN}-neko-rooms.conf"
systemctl reload nginx || true
fi
# 4. Remove .htpasswd file
if [ "$CREATE_HTPASSWD" == true ]; then
echo -e "${YELLOW}Removing .htpasswd file...${NC}"
rm -f "/etc/nginx/.htpasswd"
fi
# 5. Remove Certbot certificates and configuration
if [ "$OBTAIN_CERTIFICATE" == true ]; then
echo -e "${YELLOW}Removing generated certificates...${NC}"
certbot delete --cert-name "$DOMAIN" -n || true
rm -f "/etc/letsencrypt/renewal/${DOMAIN}.conf" || true
fi
# 6. Remove the cron job
if [ "$CREATE_CRON_JOB" == true ]; then
echo -e "${YELLOW}Removing Certbot cron job...${NC}"
rm -f "/etc/cron.d/certbot-renew"
systemctl restart cron || true
fi
# 7. Remove the manage_htpasswd script
if [ "$CREATE_MANAGE_SCRIPT" == true ]; then
echo -e "${YELLOW}Removing manage_htpasswd.sh...${NC}"
rm -f "/usr/local/bin/manage_htpasswd.sh"
fi
echo -e "${GREEN}================================================${NC}"
echo -e "${RED}Rollback complete. Exiting.${NC}"
echo -e "${GREEN}================================================${NC}"
exit 1
}
# Function to get all server IPs
get_server_ips() {
local ip_list
local fallback_ip_service="checkip.amazonaws.com"
ip_list=$(hostname -I | awk '{for (i=1; i<=NF; i++) if ($i !~ /^172\./ && $i !~ /^10\./ && $i !~ /^192\.168\./) print $i}' | tr '\n' ', ' | sed 's/, $//')
# If the hostname command fails, use the following methods
if [ -z "$ip_list" ]; then
ip_list=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null)
if [ $? -ne 0 ]; then
echo "dig command failed, falling back to $fallback_ip_service" >&2
fi
fi
if [ -z "$ip_list" ]; then
ip_list=$(curl -s $fallback_ip_service)
fi
echo "$ip_list"
}
# Get all server IPs
SERVER_IPS=$(get_server_ips)
# Introduction message
echo -e "${GREEN}==================================================${NC}"
echo -e "${GREEN}Neko Rooms Installer${NC} ${AQUA}by H1ghSyst3m${NC}"
echo -e "This scipt uses nginx as reverse proxy, certbot and docker to install Neko Rooms."
echo -e "${GREEN}==================================================${NC}"
echo -e "${YELLOW}This script will:${NC}"
echo -e " 1. Install Docker, Docker Compose, NGINX, and Certbot."
echo -e " 2. Deploy Neko Rooms using Docker Compose."
echo -e " 3. Configure NGINX as a reverse proxy."
echo -e " 4. Obtain and configure an SSL certificate."
echo -e " 5. Add a cron job for automatic SSL renewal."
echo -e "${GREEN}==================================================${NC}"
echo -e "${YELLOW}Please ensure the following before proceeding:${NC}"
echo -e " - ${GREEN}OS:${NC}"
echo -e " - Kernel version 2 or higher."
echo -e " - Debian 9 or higher."
echo -e " - Ubuntu 18.04 or higher."
echo -e " - ${GREEN}Hardware:${NC}"
echo -e " - Memory at least 2GB."
echo -e " - CPU at least 4 cores."
echo -e " - Disk at least 8GB."
echo -e " - ${GREEN}Network:${NC}"
echo -e " - Public IP: $SERVER_IPS"
echo -e " - Free TCP ports: 80 and 443."
echo -e " - Free UDP port range: 59000-59100."
echo -e " - Domain name pointing to your IP: $SERVER_IPS"
echo -e " - Run this script as superuser."
echo -e "${GREEN}==================================================${NC}\n"
# Detect Debian users running the script with "sh" instead of bash
if readlink /proc/"$$"/exe | grep -q "dash"; then
echo 'This installer needs to be run with "bash", not "sh".' >&2
exit 1
fi
# Check if the script is run as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root or use sudo.${NC}"
exit 1
fi
# Function to ask the user to continue
ask_to_continue() {
while true; do
echo -e "${YELLOW}Do you want to continue? (yes/no)${NC}"
read -r response
case "$response" in
[Yy]* ) break ;;
[Nn]* )
echo -e "${RED}Installation aborted by user.${NC}"
exit 1 ;;
* )
echo -e "${RED}Invalid input. Please answer yes or no.${NC}" ;;
esac
done
}
# Function to detect OS and Kernel
detect_os_and_kernel() {
# Detect OS
if grep -qs "ubuntu" /etc/os-release; then
OS_VERSION="$(grep 'VERSION_ID' /etc/os-release | cut -d '"' -f 2 | tr -d '.')"
if [[ "${OS_VERSION}" -lt 1804 ]]; then
echo "Ubuntu 18.04 or higher is required to use this installer." >&2
echo "This version of Ubuntu is too old and unsupported." >&2
exit 1
fi
elif [[ -e /etc/debian_version ]]; then
OS_VERSION="$(grep -oE '[0-9]+' /etc/debian_version | head -1)"
if [[ "${OS_VERSION}" -lt 9 ]]; then
echo "Debian 9 or higher is required to use this installer." >&2
echo "This version of Debian is too old and unsupported." >&2
exit 1
fi
else
echo "This installer seems to be running on an unsupported distribution." >&2
echo "Supported distributions are Ubuntu and Debian." >&2
exit 1
fi
# Detect Kernel
if [[ "$(uname -r | cut -d "." -f 1)" -eq 2 ]]; then
echo "The system is running an old kernel, which is incompatible with this installer." >&2
exit 1
fi
}
# Function to check and install required packages
install_required_packages() {
echo -e "${YELLOW}Checking and installing required packages: dnsutils and git...${NC}"
# Update package list and install required packages
apt update -y >/dev/null 2>&1
apt install -y dnsutils git >/dev/null 2>&1
# Verify installation of `dnsutils`
if ! command -v dig &>/dev/null; then
echo -e "${RED}Failed to install dnsutils. Please check your network or package manager settings.${NC}"
exit 1
fi
# Verify installation of `git`
if ! command -v git &>/dev/null; then
echo -e "${RED}Failed to install git. Please check your network or package manager settings.${NC}"
exit 1
fi
echo -e "${GREEN}Required packages installed successfully.${NC}"
}
# Function to validate user input
validate_input() {
local prompt="$1"
local var_name="$2"
local default="$3"
while true; do
echo -e "${YELLOW}${prompt}${NC} ${default:+[Default: $default]}"
read input
input="${input:-$default}"
if [ -z "$input" ]; then
echo -e "${RED}Input cannot be empty. Please try again.${NC}"
else
eval "$var_name=\"$input\""
break
fi
done
}
# Function to validate email
validate_email() {
local email="$1"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Function to validate domain
validate_domain() {
local domain="$1"
if [[ "$domain" =~ ^(([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)\.)+[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Function to validate and get domain
validate_and_get_domain() {
while true; do
validate_input "Enter your domain name (e.g., watch.example.com, example.com):" DOMAIN
if validate_domain "$DOMAIN"; then
# Get the A record of the domain
DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n 1)
# Convert SERVER_IPS into a line-separated list for better matching
SERVER_IPS_LIST=$(echo "$SERVER_IPS" | tr ',' '\n' | tr -d ' ')
# Check if the domain IP matches any server IP
if echo "$SERVER_IPS_LIST" | grep -q "^$DOMAIN_IP$"; then
echo -e "${GREEN}Domain $DOMAIN is correctly pointing to the server's IP: $DOMAIN_IP.${NC}"
break
else
echo -e "${RED}Domain $DOMAIN does not point to any of the server's IPs.${NC}"
echo -e "${YELLOW}Current IP of $DOMAIN: ${DOMAIN_IP:-Not Found}${NC}"
echo -e "${YELLOW}Your Server IPs: $SERVER_IPS${NC}"
echo -e "${YELLOW}Please update the domain's A record to point to one of the server's IPs and try again.${NC}"
echo -e ""
# Ask the user if they want to continue with the domain anyway
while true; do
echo -e "${YELLOW}Do you want to continue with this domain anyway? (yes/no)${NC}"
read -r response
case "$response" in
[Yy]* ) echo -e "${YELLOW}Continuing with domain $DOMAIN despite the IP mismatch.${NC}"; break 2 ;;
[Nn]* ) echo -e "${RED}Please update the domain's A record and try again.${NC}"; break ;;
* ) echo -e "${RED}Invalid input. Please answer yes or no.${NC}" ;;
esac
done
fi
else
echo -e "${RED}Invalid domain name. Please enter a valid domain.${NC}"
fi
done
}
# Function to validate and get email
validate_and_get_email() {
while true; do
validate_input "Enter your email for SSL certificate:" EMAIL
if validate_email "$EMAIL"; then
break
else
echo -e "${RED}Invalid email address. Please enter a valid email.${NC}"
fi
done
}
# Function to confirm passwords
confirm_password() {
local prompt="$1"
local var_name="$2"
while true; do
echo -e "${YELLOW}${prompt}:${NC}"
read -s password1
echo -e "${YELLOW}Confirm password:${NC}"
read -s password2
if [ "$password1" != "$password2" ]; then
echo -e "${RED}Passwords do not match. Please try again.${NC}"
else
eval "$var_name=\"$password1\""
break
fi
done
}
# Function to validate and check Room port range
check_ports() {
local port_range="$1"
IFS='-' read -r start_port end_port <<<"$port_range"
if [[ ! "$start_port" =~ ^[0-9]+$ ]] || [[ ! "$end_port" =~ ^[0-9]+$ ]] || ((start_port > end_port)); then
echo -e "${RED}Invalid port range: $port_range. Please use the format START-END (e.g., 59000-59100).${NC}"
return 1
fi
for ((port = start_port; port <= end_port; port++)); do
if ss -lnu | grep -q ":$port "; then
echo -e "${RED}Port $port is already in use. Please choose a different range.${NC}"
return 1
fi
done
echo -e "${GREEN}Ports $port_range are free.${NC}"
return 0
}
# Function to validate a single port
validate_port() {
local port="$1"
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
return 0
else
return 1
fi
}
# Check if a port is available
check_port_availability() {
local port="$1"
if ss -ln | grep -q ":$port "; then
return 1
else
return 0
fi
}
# Function to validate and check Room port range
validate_room_port_range() {
while true; do
validate_input "Enter the Room port range [Default: 59000-59100]:" ROOM_PORT_RANGE "59000-59100"
if check_ports "$ROOM_PORT_RANGE"; then
break
else
echo -e "${YELLOW}Please enter a valid and free port range.${NC}"
fi
done
}
# Function to validate and check Docker port
validate_docker_port() {
while true; do
validate_input "Enter the Docker port for Neko Rooms [Default: 8080]:" DOCKER_PORT "8080"
if validate_port "$DOCKER_PORT" && check_port_availability "$DOCKER_PORT"; then
echo -e "${GREEN}Port $DOCKER_PORT is available.${NC}"
break
else
echo -e "${RED}Port $DOCKER_PORT is either invalid or already in use. Please choose another port.${NC}"
fi
done
}
# Summarize Configuration
summary() {
echo -e "\n${GREEN}Configuration Summary:${NC}"
echo -e "${YELLOW}Domain:${NC} $DOMAIN"
echo -e "${YELLOW}Email:${NC} $EMAIL"
echo -e "${YELLOW}Timezone:${NC} $TIMEZONE"
echo -e "${YELLOW}Room Port Range:${NC} $ROOM_PORT_RANGE"
echo -e "${YELLOW}Docker Port:${NC} $DOCKER_PORT"
echo -e "${YELLOW}Rooms Path Prefix:${NC} $PATH_PREFIX"
echo -e "${YELLOW}Pointed IP:${NC} $DOMAIN_IP"
echo -e "\n${GREEN}Please review the above settings carefully.${NC}"
}
ask_to_continue
detect_os_and_kernel
echo ""
install_required_packages
echo ""
echo ""
validate_and_get_domain
echo ""
validate_and_get_email
echo ""
validate_input "Enter admin username for authentication:" ADMIN_USER
echo ""
confirm_password "Enter admin password for authentication" ADMIN_PASSWORD
echo ""
validate_input "Enter your timezone (e.g., Europe/Vienna) [Default: UTC]:" TIMEZONE "UTC"
echo ""
validate_room_port_range
echo ""
validate_docker_port
echo ""
echo -e "${YELLOW}Choose a path prefix for accessing Neko Rooms.${NC}"
echo -e "${YELLOW}Example: If you choose 'room', rooms will be accessible at:${NC} ${GREEN}https://${DOMAIN}/room/<room-id>${NC}"
validate_input "Enter your path prefix (e.g., room, home, browser) [Default: room]:" PATH_PREFIX "room"
summary
echo ""
ask_to_continue
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
install_packages() {
local packages=$@
echo -e "${YELLOW}Installing required packages: $packages...${NC}"
# Start installation in the background
(apt update && apt install -y $packages) &
# Get the process ID and show the spinner
show_spinner $!
}
# Precheck for Docker
check_docker() {
if command_exists docker; then
echo -e "${GREEN}Docker is already installed.${NC}"
else
echo -e "${YELLOW}Docker is not installed. Installing Docker...${NC}"
curl -sSL https://get.docker.com/ | CHANNEL=stable bash
systemctl enable docker
systemctl start docker
fi
}
# Precheck for Docker Compose
check_docker_compose() {
if command_exists docker-compose; then
echo -e "${GREEN}Docker Compose is already installed.${NC}"
else
echo -e "${YELLOW}Docker Compose is not installed. Installing Docker Compose...${NC}"
DOCKER_COMPOSE_VERSION="1.29.2"
curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
fi
}
# Precheck for Crontab
check_crontab() {
if command -v crontab >/dev/null 2>&1; then
echo -e "${GREEN}Crontab is already installed.${NC}"
else
echo -e "${YELLOW}Crontab is not installed. Installing...${NC}"
apt update
apt install -y cron
systemctl enable cron
systemctl start cron
fi
}
# Precheck for NGINX
check_nginx() {
if command_exists nginx; then
echo -e "${GREEN}NGINX is already installed.${NC}"
else
echo -e "${YELLOW}NGINX is not installed. Installing NGINX...${NC}"
apt update
apt install -y nginx
systemctl enable nginx
systemctl start nginx
fi
}
# Precheck and Installation for Docker, Docker Compose, crontab and NGINX
echo -e "${AQUA}\n==================================================${NC}"
check_docker
echo -e "${AQUA}\n==================================================${NC}"
check_docker_compose
echo -e "${AQUA}\n==================================================${NC}"
check_crontab
echo -e "${AQUA}\n==================================================${NC}"
check_nginx
echo -e "${AQUA}\n==================================================${NC}"
# Install other dependencies
echo -e "${YELLOW}Installing required packages...${NC}"
apt update
apt install -y certbot python3-certbot-nginx apache2-utils
echo -e "${AQUA}\n==================================================${NC}"
# Clone Neko Rooms repository
echo -e "${YELLOW}Cloning Neko Rooms repository...${NC}"
git clone https://github.com/m1k1o/neko-rooms.git /opt/neko-rooms || echo -e "${GREEN}Neko Rooms repository already exists. Skipping...${NC}"
cd /opt/neko-rooms
CLONE_NEKO_ROOMS=true
echo -e "${AQUA}\n==================================================${NC}"
# Modify docker-compose.yml
echo -e "${YELLOW}Configuring Neko Rooms${NC} ${AQUA}docker-compose.yml${NC}${YELLOW}...${NC}"
cat <<EOF > docker-compose.yml
version: "3.5"
networks:
default:
attachable: true
name: "neko-rooms-net"
services:
neko-rooms:
image: "m1k1o/neko-rooms:latest"
restart: "unless-stopped"
environment:
- "TZ=${TIMEZONE}"
- "NEKO_ROOMS_MUX=true"
- "NEKO_ROOMS_EPR=${ROOM_PORT_RANGE}"
- "NEKO_ROOMS_NAT1TO1=${DOMAIN_IP}"
- "NEKO_ROOMS_INSTANCE_URL=https://${DOMAIN}/"
- "NEKO_ROOMS_INSTANCE_NETWORK=neko-rooms-net"
- "NEKO_ROOMS_TRAEFIK_ENABLED=false"
- "NEKO_ROOMS_PATH_PREFIX=/${PATH_PREFIX}/"
- "NEKO_ROOMS_STORAGE_ENABLED=true"
- "NEKO_ROOMS_STORAGE_INTERNAL=/data"
- "NEKO_ROOMS_STORAGE_EXTERNAL=/opt/neko-rooms/data"
ports:
- "127.0.0.1:${DOCKER_PORT}:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "/opt/neko-rooms/data:/data"
EOF
CREATE_DOCKER_COMPOSE=true
echo -e "${GREEN} Neko Rooms configuration updated.${NC}"
echo -e "${AQUA}\n==================================================${NC}"
# Start Neko Rooms
echo -e "${GREEN}Starting Neko Rooms service...${NC}"
docker-compose up -d
echo -e "${AQUA}\n==================================================${NC}"
# Configure NGINX without SSL
echo -e "${YELLOW}Configuring NGINX without SSL...${NC}"
cat <<EOF > /etc/nginx/sites-available/${DOMAIN}-neko-rooms.conf
map \$http_upgrade \$connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ${DOMAIN};
location ^~ /${PATH_PREFIX}/ {
proxy_pass http://127.0.0.1:${DOCKER_PORT};
proxy_set_header Host \$host;
proxy_http_version 1.1;
# WebSocket headers
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$connection_upgrade;
# Forwarding headers for reverse proxy
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 4h;
proxy_read_timeout 4h;
send_timeout 900s;
proxy_request_buffering off;
proxy_buffering off;
}
# Admin Panel Restricted Access
location / {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:${DOCKER_PORT};
proxy_set_header Host \$host;
proxy_http_version 1.1;
# Falls das Admin-UI WebSockets nutzt, sind wir vorbereitet
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$connection_upgrade;
# Forwarding headers wie oben
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
# Enable the NGINX configuration
ln -sf /etc/nginx/sites-available/${DOMAIN}-neko-rooms.conf /etc/nginx/sites-enabled/
CREATE_NGINX_CONFIG=true
systemctl reload nginx
echo -e "${GREEN}NGINX configuration updated and reloaded.${NC}"
echo -e "${AQUA}\n==================================================${NC}"
# Create a password file for admin access
echo -e "${GREEN}Creating admin authentication...${NC}"
htpasswd -cb /etc/nginx/.htpasswd ${ADMIN_USER} ${ADMIN_PASSWORD}
CREATE_HTPASSWD=true
echo -e "${AQUA}\n==================================================${NC}"
# Obtain SSL certificate and configure NGINX for SSL
echo -e "${GREEN}Obtaining SSL certificate and updating NGINX for SSL...${NC}"
certbot --nginx -d ${DOMAIN} --non-interactive --agree-tos -m ${EMAIL}
OBTAIN_CERTIFICATE=true
systemctl reload nginx
echo -e "${AQUA}\n==================================================${NC}"
# Create a separate crontab file for Certbot renewal
echo -e "${GREEN}Creating a separate crontab file for Certbot auto-renewal...${NC}"
cat <<EOF > /etc/cron.d/certbot-renew
# Cron job for Certbot auto-renewal
0 3 * * 0 root certbot renew --quiet && systemctl reload nginx
EOF
CREATE_CRON_JOB=true
# Set appropriate permissions
chmod 644 /etc/cron.d/certbot-renew
# Reload cron to apply changes
echo -e "${GREEN}Reloading cron daemon to apply the new cron job...${NC}"
systemctl restart cron
# Verify the cron job
echo -e "${GREEN}Cron job file created at /etc/cron.d/certbot-renew:${NC}"
cat /etc/cron.d/certbot-renew
echo -e "${AQUA}\n==================================================${NC}"
# Generate a script to manage admin users
echo -e "${GREEN}Generating script to manage admin users...${NC}"
cat <<'EOF' > /usr/local/bin/manage_htpasswd.sh
#!/bin/bash
HTPASSWD_FILE="/etc/nginx/.htpasswd"
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Check if htpasswd exists
if ! command -v htpasswd &>/dev/null; then
echo -e "${RED}Error: htpasswd command not found. Please install apache2-utils.${NC}"
exit 1
fi
# Function to display usage
usage() {
echo -e "${YELLOW}Usage:${NC} $0 {add|remove|list|change-password} [username]"
echo -e "${YELLOW}Commands:${NC}"
echo -e " ${GREEN}add${NC} Add a new user or update an existing user's password"
echo -e " ${GREEN}remove${NC} Remove a user"
echo -e " ${GREEN}list${NC} List all users"
echo -e " ${GREEN}change-password${NC} Change the password of an existing user"
exit 1
}
# Ensure the .htpasswd file exists
if [ ! -f "$HTPASSWD_FILE" ]; then
echo -e "${YELLOW}The .htpasswd file does not exist. Creating a new one...${NC}"
touch "$HTPASSWD_FILE"
fi
# Function to prompt for a password
prompt_password() {
while true; do
echo -e "${YELLOW}Enter password:${NC}"
read -rsp "Password: " password1
echo
echo -e "${YELLOW}Confirm password:${NC}"
read -rsp "Confirm Password: " password2
echo
if [ "$password1" != "$password2" ]; then
echo -e "${RED}Passwords do not match. Please try again.${NC}"
elif [ -z "$password1" ]; then
echo -e "${RED}Password cannot be empty. Please try again.${NC}"
else
echo "$password1"
return 0
fi
done
}
# Add or update a user
add_user() {
local username="$1"
echo -e "${YELLOW}Adding or updating user '$username'.${NC}"
sudo htpasswd "$HTPASSWD_FILE" "$username"
if [ $? -eq 0 ]; then
echo -e "${GREEN}User '$username' has been added or updated successfully.${NC}"
else
echo -e "${RED}Failed to add or update user '$username'. Please check your inputs.${NC}"
exit 1
fi
}
# Change user password
change_password() {
local username="$1"
if ! grep -q "^$username:" "$HTPASSWD_FILE"; then
echo -e "${RED}Error: User '$username' does not exist.${NC}"
exit 1
fi
echo -e "${YELLOW}Changing password for user '$username'.${NC}"
sudo htpasswd "$HTPASSWD_FILE" "$username"
if [ $? -eq 0 ]; then
echo -e "${GREEN}Password for user '$username' has been updated successfully.${NC}"
else
echo -e "${RED}Failed to update password for user '$username'. Please check your inputs.${NC}"
exit 1
fi
}
# Remove a user
remove_user() {
local username="$1"
if ! grep -q "^$username:" "$HTPASSWD_FILE"; then
echo -e "${RED}Error: User '$username' does not exist.${NC}"
exit 1
fi
sed -i "/^$username:/d" "$HTPASSWD_FILE"
echo -e "${GREEN}User '$username' has been removed.${NC}"
}
# List all users
list_users() {
if [ ! -s "$HTPASSWD_FILE" ]; then
echo -e "${YELLOW}No users found in the .htpasswd file.${NC}"
else
echo -e "${GREEN}Users in the .htpasswd file:${NC}"
cut -d: -f1 "$HTPASSWD_FILE"
fi
}
# Main logic
case "$1" in
add)
if [ -z "$2" ]; then
echo -e "${RED}Error: Username is required for the 'add' command.${NC}"
usage
fi
add_user "$2"
;;
remove)
if [ -z "$2" ]; then
echo -e "${RED}Error: Username is required for the 'remove' command.${NC}"
usage
fi
remove_user "$2"
;;
list)
list_users
;;
change-password)
if [ -z "$2" ]; then
echo -e "${RED}Error: Username is required for the 'change-password' command.${NC}"
usage
fi
change_password "$2"
;;
*)
usage
;;
esac
EOF
CREATE_MANAGE_SCRIPT=true
# Make the script executable
chmod +x /usr/local/bin/manage_htpasswd.sh
echo -e "${GREEN}Script generated: /usr/local/bin/manage_htpasswd.sh${NC}"
echo -e "${YELLOW}You can use this script to manage admin panel users:${NC}"
echo -e "${YELLOW} - Add user: /usr/local/bin/manage_htpasswd.sh add <username>${NC}"
echo -e "${YELLOW} - Remove user: /usr/local/bin/manage_htpasswd.sh remove <username>${NC}"
echo -e "${YELLOW} - Change password: /usr/local/bin/manage_htpasswd.sh change-password <username>${NC}"
echo -e "${YELLOW} - List users: /usr/local/bin/manage_htpasswd.sh list${NC}"
echo -e "${AQUA}\n==================================================${NC}"
# Final message
echo -e "${GREEN}Installation complete! Access your Neko Rooms at https://${DOMAIN}${NC}"
+4
View File
@@ -13,6 +13,10 @@ if [ -z $APP_HOST ]; then
fi
done
if [ -z $APP_HOST ]; then
APP_HOST=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
if [ -z $APP_HOST ]; then
APP_HOST=$(hostname -i 2>/dev/null)
fi
+4 -1
View File
@@ -17,6 +17,10 @@ if [ -z $NEKO_ROOMS_NAT1TO1 ]; then
fi
done
if [ -z $NEKO_ROOMS_NAT1TO1 ]; then
NEKO_ROOMS_NAT1TO1=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
if [ -z $NEKO_ROOMS_NAT1TO1 ]; then
NEKO_ROOMS_NAT1TO1=$(hostname -i 2>/dev/null)
fi
@@ -54,7 +58,6 @@ docker run --rm -it \
-e "NEKO_ROOMS_MOUNTS_WHITELIST=`realpath ${EXTERNAL_PATH}`" \
-e "NEKO_ROOMS_PATH_PREFIX=/room/" \
-e "NEKO_ROOMS_TRAEFIK_ENABLED=false" \
-e 'DOCKER_API_VERSION=1.39' \
-v "/var/run/docker.sock:/var/run/docker.sock" \
--net="${NEKO_ROOMS_INSTANCE_NETWORK}" \
--entrypoint="/app/bin/neko_rooms" \
-1
View File
@@ -31,7 +31,6 @@ docker run --rm -it \
-e "NEKO_ROOMS_TRAEFIK_DOMAIN=${NEKO_ROOMS_TRAEFIK_DOMAIN}" \
-e "NEKO_ROOMS_TRAEFIK_ENTRYPOINT=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
-e "NEKO_ROOMS_TRAEFIK_NETWORK=${NEKO_ROOMS_TRAEFIK_NETWORK}" \
-e 'DOCKER_API_VERSION=1.39' \
-v "/var/run/docker.sock:/var/run/docker.sock" \
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
-l "traefik.enable=true" \
+1 -1
View File
@@ -26,7 +26,7 @@ docker run --rm -it \
-e "TZ=${TZ}" \
-v "/var/run/docker.sock:/var/run/docker.sock" \
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
traefik:2.4 \
traefik:2 \
--api.insecure=true \
--providers.docker=true \
--providers.docker.watch=true \
+1 -1
View File
@@ -14,5 +14,5 @@ However, there are some usecases, when this might come in handy:
In order to pull new images run:
```sh
docker-compose exec neko-rooms docker pull m1k1o/neko:chromium
docker-compose exec neko-rooms docker pull ghcr.io/m1k1o/neko/chromium
```
+3 -3
View File
@@ -29,7 +29,7 @@ docker run -d \
-p "80:80" \
-v "/var/run/docker.sock:/var/run/docker.sock:ro" \
-e "TZ=${TZ}" \
traefik:2.4 \
traefik:2 \
--providers.docker=true \
--providers.docker.watch=true \
--providers.docker.exposedbydefault=false \
@@ -37,8 +37,8 @@ docker run -d \
--entrypoints.web.address=:80;
# pull some neko images...
docker pull m1k1o/neko:firefox
docker pull m1k1o/neko:chromium
docker pull ghcr.io/m1k1o/neko/firefox
docker pull ghcr.io/m1k1o/neko/chromium
#
# (re)start neko-rooms
+55 -33
View File
@@ -1,48 +1,70 @@
module github.com/m1k1o/neko-rooms
go 1.21
go 1.24.0
toolchain go1.24.1
require (
github.com/docker/cli v24.0.6+incompatible
github.com/docker/docker v24.0.6+incompatible
github.com/docker/go-connections v0.4.0
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
github.com/rs/zerolog v1.31.0
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
github.com/containerd/errdefs v1.0.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/docker v28.4.0+incompatible
github.com/docker/go-connections v0.6.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/prometheus/client_golang v1.23.2
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gotest.tools/v3 v3.5.2 // indirect
)
+148 -514
View File
@@ -1,551 +1,185 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+7
View File
@@ -58,8 +58,15 @@ func (manager *ApiManagerCtx) Mount(r chi.Router) {
r.Post("/start", manager.roomGenericAction(manager.rooms.Start))
r.Post("/stop", manager.roomGenericAction(manager.rooms.Stop))
r.Post("/restart", manager.roomGenericAction(manager.rooms.Restart))
r.Post("/pause", manager.roomGenericAction(manager.rooms.Pause))
r.Post("/recreate", manager.roomRecreate)
})
r.Get("/docker-compose.yaml", manager.dockerCompose)
//
// events
//
r.Get("/events", manager.events)
}
+67
View File
@@ -0,0 +1,67 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
func (manager *ApiManagerCtx) events(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
sse := r.URL.Query().Has("sse")
if sse {
w.Header().Set("Content-Type", "text/event-stream")
} else {
w.Header().Set("Content-Type", "application/json")
}
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Connection does not support streaming", http.StatusBadRequest)
return
}
var ping <-chan time.Time
if !sse {
// dummy channel, never ping
ping = make(<-chan time.Time)
} else {
// ping every 1 minute
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
ping = ticker.C
}
// listen for room events
events, errs := manager.rooms.Events(r.Context())
for {
select {
case <-ping:
fmt.Fprintf(w, ": ping\n\n")
flusher.Flush()
case _, ok := <-errs:
if !ok {
manager.logger.Debug().Msg("sse channel closed")
}
return
case e := <-events:
jsonData, err := json.Marshal(e)
if err != nil {
manager.logger.Err(err).Msg("failed to marshal event")
continue
}
if sse {
fmt.Fprintf(w, "event: rooms\n")
fmt.Fprintf(w, "data: %s\n\n", jsonData)
} else {
fmt.Fprintf(w, "rooms\t%s\n", jsonData)
}
flusher.Flush()
}
}
}
+23 -14
View File
@@ -32,6 +32,7 @@ type Room struct {
PathPrefix string
Labels []string
WaitEnabled bool
StopTimeoutSec int
StorageEnabled bool
StorageInternal string
@@ -63,18 +64,20 @@ func (Room) Init(cmd *cobra.Command) error {
}
cmd.PersistentFlags().StringSlice("neko_images", []string{
"m1k1o/neko:firefox",
"m1k1o/neko:chromium",
"m1k1o/neko:google-chrome",
"m1k1o/neko:ungoogled-chromium",
"m1k1o/neko:microsoft-edge",
"m1k1o/neko:brave",
"m1k1o/neko:vivaldi",
"m1k1o/neko:tor-browser",
"m1k1o/neko:vlc",
"m1k1o/neko:remmina",
"m1k1o/neko:xfce",
"m1k1o/neko:kde",
"ghcr.io/m1k1o/neko/firefox",
"ghcr.io/m1k1o/neko/waterfox",
"ghcr.io/m1k1o/neko/tor-browser",
"ghcr.io/m1k1o/neko/chromium",
"ghcr.io/m1k1o/neko/google-chrome",
"ghcr.io/m1k1o/neko/ungoogled-chromium",
"ghcr.io/m1k1o/neko/microsoft-edge",
"ghcr.io/m1k1o/neko/brave",
"ghcr.io/m1k1o/neko/vivaldi",
"ghcr.io/m1k1o/neko/opera",
"ghcr.io/m1k1o/neko/remmina",
"ghcr.io/m1k1o/neko/vlc",
"ghcr.io/m1k1o/neko/xfce",
"ghcr.io/m1k1o/neko/kde",
}, "neko images to be used")
if err := viper.BindPFlag("neko_images", cmd.PersistentFlags().Lookup("neko_images")); err != nil {
return err
@@ -100,6 +103,11 @@ func (Room) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().Int("stop_timeout", 10, "timeout in seconds for stopping the room with SIGTERM, after that SIGKILL is used (0 to disable, -1 to wait forever)")
if err := viper.BindPFlag("stop_timeout", cmd.PersistentFlags().Lookup("stop_timeout")); err != nil {
return err
}
// Data
cmd.PersistentFlags().Bool("storage.enabled", true, "whether storage is enabled, where peristent containers data will be stored")
@@ -207,6 +215,7 @@ func (s *Room) Set() {
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
s.Labels = viper.GetStringSlice("labels")
s.WaitEnabled = viper.GetBool("wait_enabled")
s.StopTimeoutSec = viper.GetInt("stop_timeout")
s.StorageEnabled = viper.GetBool("storage.enabled")
s.StorageInternal = viper.GetString("storage.internal")
@@ -310,8 +319,8 @@ func (s *Room) GetInstanceUrl() url.URL {
func (s *Room) GetRoomUrl(roomName string) string {
instanceUrl := s.GetInstanceUrl()
if strings.HasPrefix(instanceUrl.Host, "*.") {
instanceUrl.Host = roomName + "." + strings.TrimPrefix(instanceUrl.Host, "*.")
if after, ok := strings.CutPrefix(instanceUrl.Host, "*."); ok {
instanceUrl.Host = roomName + "." + after
} else {
instanceUrl.Path = path.Join(instanceUrl.Path, s.PathPrefix, roomName) + "/"
}
+13 -6
View File
@@ -16,12 +16,13 @@ type Admin struct {
}
type Server struct {
Cert string
Key string
Bind string
Proxy bool
CORS bool
PProf bool
Cert string
Key string
Bind string
Proxy bool
CORS bool
PProf bool
Metrics bool
Admin Admin
}
@@ -57,6 +58,11 @@ func (Server) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().Bool("metrics", false, "enable metrics endpoint available at /metrics")
if err := viper.BindPFlag("metrics", cmd.PersistentFlags().Lookup("metrics")); err != nil {
return err
}
// Admin
cmd.PersistentFlags().String("admin.static", "", "path to neko_rooms admin client files to serve")
@@ -94,6 +100,7 @@ func (s *Server) Set() {
s.Proxy = viper.GetBool("proxy")
s.CORS = viper.GetBool("cors")
s.PProf = viper.GetBool("pprof")
s.Metrics = viper.GetBool("metrics")
s.Admin.Static = viper.GetString("admin.static")
s.Admin.PathPrefix = path.Join("/", path.Clean(viper.GetString("admin.path_prefix")))
+4 -4
View File
@@ -14,7 +14,7 @@ import (
var policiesJson string
func Generate(policies types.BrowserPolicyContent) (string, error) {
policiesTmpl := map[string]interface{}{}
policiesTmpl := map[string]any{}
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
return "", err
}
@@ -23,7 +23,7 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
// Extensions
//
ExtensionInstallForcelist := []interface{}{}
ExtensionInstallForcelist := []any{}
for _, e := range policies.Extensions {
URL := e.URL
if URL == "" {
@@ -36,7 +36,7 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
)
}
ExtensionInstallAllowlist := []interface{}{}
ExtensionInstallAllowlist := []any{}
for _, e := range policies.Extensions {
ExtensionInstallAllowlist = append(
ExtensionInstallAllowlist,
@@ -46,7 +46,7 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
policiesTmpl["ExtensionInstallForcelist"] = ExtensionInstallForcelist
policiesTmpl["ExtensionInstallAllowlist"] = ExtensionInstallAllowlist
policiesTmpl["ExtensionInstallBlocklist"] = []interface{}{"*"}
policiesTmpl["ExtensionInstallBlocklist"] = []any{"*"}
//
// Developer Tools
+2 -2
View File
@@ -12,7 +12,7 @@ import (
func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
policies := types.BrowserPolicyContent{}
policiesTmpl := map[string]interface{}{}
policiesTmpl := map[string]any{}
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
return nil, err
}
@@ -23,7 +23,7 @@ func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
if extensions, ok := policiesTmpl["ExtensionInstallForcelist"]; ok {
policies.Extensions = []types.BrowserPolicyExtension{}
for _, val := range extensions.([]interface{}) {
for _, val := range extensions.([]any) {
s := strings.Split(val.(string), ";")
url := ""
if len(s) > 1 {
+7 -7
View File
@@ -14,7 +14,7 @@ var policiesJson string
func Generate(policies types.BrowserPolicyContent) (string, error) {
policiesTmpl := struct {
Policies map[string]interface{} `json:"policies"`
Policies map[string]any `json:"policies"`
}{}
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
return "", err
@@ -24,13 +24,13 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
// Extensions
//
ExtensionSettings := map[string]interface{}{}
ExtensionSettings["*"] = map[string]interface{}{
ExtensionSettings := map[string]any{}
ExtensionSettings["*"] = map[string]any{
"installation_mode": "blocked",
}
for _, e := range policies.Extensions {
ExtensionSettings[e.ID] = map[string]interface{}{
ExtensionSettings[e.ID] = map[string]any{
"install_url": e.URL,
"installation_mode": "force_installed",
}
@@ -48,18 +48,18 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
// Persistent Data
//
Preferences := policiesTmpl.Policies["Preferences"].(map[string]interface{})
Preferences := policiesTmpl.Policies["Preferences"].(map[string]any)
Preferences["browser.urlbar.suggest.history"] = policies.PersistentData
Preferences["places.history.enabled"] = policies.PersistentData
policiesTmpl.Policies["Preferences"] = Preferences
policiesTmpl.Policies["SanitizeOnShutdown"] = !policies.PersistentData
if policies.PersistentData {
policiesTmpl.Policies["Homepage"] = map[string]interface{}{
policiesTmpl.Policies["Homepage"] = map[string]any{
"StartPage": "previous-session",
}
} else {
policiesTmpl.Policies["Homepage"] = map[string]interface{}{
policiesTmpl.Policies["Homepage"] = map[string]any{
"StartPage": "homepage",
}
}
+3 -3
View File
@@ -13,7 +13,7 @@ func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
policies := types.BrowserPolicyContent{}
policiesTmpl := struct {
Policies map[string]interface{} `json:"policies"`
Policies map[string]any `json:"policies"`
}{}
if err := json.Unmarshal([]byte(policiesJson), &policiesTmpl); err != nil {
return nil, err
@@ -30,12 +30,12 @@ func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
if extensions, ok := policiesTmpl.Policies["ExtensionSettings"]; ok {
policies.Extensions = []types.BrowserPolicyExtension{}
for id, val := range extensions.(map[string]interface{}) {
for id, val := range extensions.(map[string]any) {
if id == "*" {
continue
}
data := val.(map[string]interface{})
data := val.(map[string]any)
url, _ := data["install_url"].(string)
policies.Extensions = append(
+35 -1
View File
@@ -59,6 +59,8 @@ func RoomNotFound(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
if waitEnabled {
roomWait(w, r)
} else {
w.Write([]byte(`<meta http-equiv="refresh" content="60">`))
}
}
@@ -81,10 +83,36 @@ func RoomNotRunning(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
if waitEnabled {
roomWait(w, r)
} else {
w.Write([]byte(`<meta http-equiv="refresh" content="10">`))
}
}
func RoomPaused(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
utils.Swal2Response(w, `
<div class="swal2-header">
<div class="swal2-icon swal2-warning">
<div class="swal2-icon-content">!</div>
</div>
<h2 class="swal2-title">Room is paused!</h2>
</div>
<div class="swal2-content">
<div>The room you are trying to join is paused.</div>
<div>You can wait on this page until it will be unpaused.</div>
</div>
<div class="swal2-actions">
<div class="swal2-loader" style="display:none;"></div>
<button type="button" onclick="location = location" class="swal2-confirm swal2-styled" style="margin-top: 1.25em">Reload</button>
</div>
`)
if waitEnabled {
roomWait(w, r)
} else {
w.Write([]byte(`<meta http-equiv="refresh" content="2">`))
}
}
func RoomNotReady(w http.ResponseWriter, r *http.Request) {
func RoomNotReady(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
utils.Swal2Response(w, `
<meta http-equiv="refresh" content="2">
@@ -102,6 +130,12 @@ func RoomNotReady(w http.ResponseWriter, r *http.Request) {
<button type="button" onclick="location = location" class="swal2-confirm swal2-styled" style="margin-top: 1.25em">Reload</button>
</div>
`)
if waitEnabled {
roomWait(w, r)
} else {
w.Write([]byte(`<meta http-equiv="refresh" content="2">`))
}
}
func RoomReady(w http.ResponseWriter, r *http.Request) {
+78 -152
View File
@@ -2,7 +2,6 @@ package proxy
import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
@@ -11,19 +10,20 @@ import (
"strings"
"sync"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/m1k1o/neko-rooms/internal/room"
"github.com/m1k1o/neko-rooms/internal/types"
"github.com/m1k1o/neko-rooms/pkg/prefix"
)
type entry struct {
id string
running bool
handler *httputil.ReverseProxy
ready bool
paused bool
handler http.Handler
}
type wait struct {
@@ -41,20 +41,18 @@ type ProxyManagerCtx struct {
waitChans map[string]*wait
waitEnabled bool
client *dockerClient.Client
instanceName string
handlers prefix.Tree[*entry]
rooms *room.RoomManagerCtx
handlers prefix.Tree[*entry]
}
func New(client *dockerClient.Client, instanceName string, waitEnabled bool) *ProxyManagerCtx {
func New(rooms *room.RoomManagerCtx, waitEnabled bool) *ProxyManagerCtx {
return &ProxyManagerCtx{
logger: log.With().Str("module", "proxy").Logger(),
waitChans: map[string]*wait{},
client: client,
instanceName: instanceName,
waitEnabled: waitEnabled,
handlers: prefix.NewTree[*entry](),
rooms: rooms,
waitEnabled: waitEnabled,
handlers: prefix.NewTree[*entry](),
}
}
@@ -67,48 +65,32 @@ func (p *ProxyManagerCtx) Start() {
p.logger.Err(err).Msg("unable to refresh containers")
}
msgs, errs := p.client.Events(p.ctx, dockerTypes.EventsOptions{
Filters: filters.NewArgs(
filters.Arg("type", "container"),
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
filters.Arg("event", "create"),
filters.Arg("event", "start"),
//filters.Arg("event", "health_status"),
filters.Arg("event", "stop"),
filters.Arg("event", "destroy"),
),
})
msgs, errs := p.rooms.Events(p.ctx)
for {
select {
case err, ok := <-errs:
if !ok {
p.logger.Fatal().Msg("docker event error channel closed")
return
}
p.logger.Err(err).Msg("got docker event error")
p.logger.Err(err).Msg("room event error")
case msg, ok := <-msgs:
if !ok {
p.logger.Fatal().Msg("docker event channel closed")
return
}
enabled, path, port, ok := p.parseLabels(msg.Actor.Attributes)
enabled, path, port, ok := p.parseLabels(msg.ContainerLabels)
if !ok {
break
}
host := msg.ID[:12] + ":" + port
host := msg.ID + ":" + port
p.logger.Info().
Str("action", msg.Action).
Str("action", string(msg.Action)).
Str("path", path).
Str("host", host).
Msg("got docker event")
Msg("got room event")
// terminate waiting for any events
if p.waitEnabled {
// terminate waiting for room ready event
if p.waitEnabled && msg.Action == types.RoomEventReady {
p.waitMu.Lock()
ch, ok := p.waitChans[path]
if ok {
@@ -120,32 +102,42 @@ func (p *ProxyManagerCtx) Start() {
p.mu.Lock()
switch msg.Action {
case "create":
case types.RoomEventCreated:
p.handlers.Insert(path, &entry{
id: msg.ID,
running: false,
})
case "start":
case types.RoomEventStarted:
p.handlers.Insert(path, &entry{
id: msg.ID,
running: true,
ready: false,
})
case types.RoomEventReady:
e := &entry{
id: msg.ID,
running: true,
ready: true,
}
// if proxying is disabled
if enabled {
e.handler = httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: host,
})
e.handler = p.newProxyHandler(path, host)
}
p.handlers.Insert(path, e)
case "stop":
case types.RoomEventStopped:
p.handlers.Insert(path, &entry{
id: msg.ID,
running: false,
})
case "destroy":
case types.RoomEventPaused:
p.handlers.Insert(path, &entry{
id: msg.ID,
running: false,
paused: true,
})
case types.RoomEventDestroyed:
p.handlers.Remove(path)
}
p.mu.Unlock()
@@ -163,41 +155,31 @@ func (p *ProxyManagerCtx) Refresh() error {
p.mu.Lock()
defer p.mu.Unlock()
containers, err := p.client.ContainerList(p.ctx, dockerTypes.ContainerListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
),
})
rooms, err := p.rooms.List(p.ctx, nil)
if err != nil {
return err
}
p.handlers = prefix.NewTree[*entry]()
for _, cont := range containers {
enabled, path, port, ok := p.parseLabels(cont.Labels)
for _, room := range rooms {
enabled, path, port, ok := p.parseLabels(room.ContainerLabels)
if !ok {
continue
}
host := cont.ID[:12] + ":" + port
host := room.ID + ":" + port
entry := &entry{
id: cont.ID,
id: room.ID,
running: room.Running,
ready: room.IsReady,
paused: room.Paused,
}
if cont.State == "running" {
entry.running = true
// if proxying is disabled
if enabled {
entry.handler = httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: host,
})
}
// if proxying is enabled and room is ready
if enabled && room.IsReady {
entry.handler = p.newProxyHandler(path, host)
}
p.handlers.Insert(path, entry)
@@ -207,41 +189,9 @@ func (p *ProxyManagerCtx) Refresh() error {
}
func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, path string, port string, ok bool) {
enabledStr, found := labels["m1k1o.neko_rooms.proxy.enabled"]
if !found {
//
// workaround for legacy traefik labels
//
// get room name
var roomName string
roomName, ok = labels["m1k1o.neko_rooms.name"]
if !ok {
return
}
// get container name
containerName := p.instanceName + "-" + roomName
// get path
path, ok = labels["traefik.http.middlewares."+containerName+"-prf.stripprefix.prefixes"]
if !ok {
return
}
// remove last /
path = strings.TrimSuffix(path, "/")
// get port
port, ok = labels["traefik.http.services."+containerName+"-frontend.loadbalancer.server.port"]
if !ok {
return
}
//
// workaround for legacy traefik labels
//
var enabledStr string
enabledStr, ok = labels["m1k1o.neko_rooms.proxy.enabled"]
if !ok {
return
}
@@ -257,6 +207,18 @@ func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, p
return
}
func (p *ProxyManagerCtx) newProxyHandler(prefix, host string) http.Handler {
handler := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: host,
})
handler.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
p.logger.Err(err).Str("prefix", prefix).Msg("proxy error")
http.Error(w, "unable to connect to room", http.StatusBadGateway)
}
return http.StripPrefix(prefix, handler)
}
func (p *ProxyManagerCtx) waitForPath(w http.ResponseWriter, r *http.Request, path string) {
p.logger.Debug().Str("path", path).Msg("adding new wait handler")
@@ -299,15 +261,23 @@ func (p *ProxyManagerCtx) ServeHTTP(w http.ResponseWriter, r *http.Request) {
proxy, prefix, ok := p.handlers.Match(cleanPath)
p.mu.RUnlock()
// if room not found
if !ok {
// blocking until room is created
// if room is not ready
if !ok || !proxy.running || !proxy.ready {
// blocking until room is ready
if r.URL.Query().Has("wait") && p.waitEnabled {
p.waitForPath(w, r, cleanPath)
return
}
RoomNotFound(w, r, p.waitEnabled)
if !ok {
RoomNotFound(w, r, p.waitEnabled)
} else if proxy.paused {
RoomPaused(w, r, p.waitEnabled)
} else if !proxy.running {
RoomNotRunning(w, r, p.waitEnabled)
} else {
RoomNotReady(w, r, p.waitEnabled)
}
return
}
@@ -318,56 +288,12 @@ func (p *ProxyManagerCtx) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// if room not running
if !proxy.running {
// blocking until room is running
if r.URL.Query().Has("wait") && p.waitEnabled {
p.waitForPath(w, r, cleanPath)
return
}
RoomNotRunning(w, r, p.waitEnabled)
return
}
// if not proxying, check for container readiness status
// if not proxying, just return room ready
if proxy.handler == nil {
containers, err := p.client.ContainerList(p.ctx, dockerTypes.ContainerListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
filters.Arg("id", proxy.id),
),
})
if err != nil || len(containers) != 1 {
p.logger.Err(err).Msg("error while getting container ready status")
RoomNotReady(w, r)
return
}
container := containers[0]
if strings.Contains(container.Status, "starting") {
RoomNotReady(w, r)
} else {
RoomReady(w, r)
}
RoomReady(w, r)
return
}
// handle not ready room
proxy.handler.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
if r.URL.Path == "/" {
RoomNotReady(w, r)
}
p.logger.Err(err).Str("prefix", prefix).Msg("proxying error")
}
// strip prefix from proxy
handler := http.StripPrefix(prefix, proxy.handler)
// handle by proxy
handler.ServeHTTP(w, r)
proxy.handler.ServeHTTP(w, r)
}
+7 -7
View File
@@ -6,17 +6,17 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"slices"
"sync"
"time"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
dockerImage "github.com/docker/docker/api/types/image"
dockerRegistry "github.com/docker/docker/api/types/registry"
dockerClient "github.com/docker/docker/client"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/m1k1o/neko-rooms/internal/types"
"github.com/m1k1o/neko-rooms/internal/utils"
)
type PullManagerCtx struct {
@@ -75,7 +75,7 @@ func (manager *PullManagerCtx) setDone() {
}
func (manager *PullManagerCtx) Start(request types.PullStart) error {
if in, _ := utils.ArrayIn(request.NekoImage, manager.images); !in {
if !slices.Contains(manager.images, request.NekoImage) {
return fmt.Errorf("unknown neko image")
}
@@ -85,9 +85,9 @@ func (manager *PullManagerCtx) Start(request types.PullStart) error {
}
// handle registry auth
var opts dockerTypes.ImagePullOptions
var opts dockerImage.PullOptions
if request.RegistryUser != "" && request.RegistryPass != "" {
authConfig := registry.AuthConfig{
authConfig := dockerRegistry.AuthConfig{
Username: request.RegistryUser,
Password: request.RegistryPass,
}
@@ -97,7 +97,7 @@ func (manager *PullManagerCtx) Start(request types.PullStart) error {
return err
}
opts = dockerTypes.ImagePullOptions{
opts = dockerImage.PullOptions{
RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
}
}
+29 -22
View File
@@ -4,32 +4,39 @@ import (
"context"
"fmt"
"io"
"strings"
"time"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/containerd/errdefs"
dockerContainer "github.com/docker/docker/api/types/container"
dockerFilters "github.com/docker/docker/api/types/filters"
"github.com/m1k1o/neko-rooms/internal/types"
)
func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container) (*types.RoomEntry, error) {
func (manager *RoomManagerCtx) containerToEntry(container dockerContainer.Summary) (*types.RoomEntry, error) {
labels, err := manager.extractLabels(container.Labels)
if err != nil {
return nil, err
}
roomId := container.ID[:12]
entry := &types.RoomEntry{
ID: container.ID[:12],
ID: roomId,
URL: labels.URL,
Name: labels.Name,
NekoImage: labels.NekoImage,
IsOutdated: labels.NekoImage != container.Image,
MaxConnections: labels.Epr.Max - labels.Epr.Min + 1,
Running: container.State == "running",
Paused: container.State == "paused",
IsReady: manager.events.IsRoomReady(roomId) || strings.Contains(container.Status, "healthy"),
Status: container.Status,
Created: time.Unix(container.Created, 0),
Labels: labels.UserDefined,
ContainerLabels: container.Labels,
}
if labels.Mux {
@@ -39,25 +46,25 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container)
return entry, nil
}
func (manager *RoomManagerCtx) listContainers(ctx context.Context, labels map[string]string) ([]dockerTypes.Container, error) {
args := filters.NewArgs(
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", manager.config.InstanceName)),
func (manager *RoomManagerCtx) listContainers(ctx context.Context, labels map[string]string) ([]dockerContainer.Summary, error) {
args := dockerFilters.NewArgs(
dockerFilters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", manager.config.InstanceName)),
)
for key, val := range labels {
args.Add("label", fmt.Sprintf("m1k1o.neko_rooms.x-%s=%s", key, val))
}
return manager.client.ContainerList(ctx, dockerTypes.ContainerListOptions{
return manager.client.ContainerList(ctx, dockerContainer.ListOptions{
All: true,
Filters: args,
})
}
func (manager *RoomManagerCtx) containerFilter(ctx context.Context, args filters.Args) (*dockerTypes.Container, error) {
func (manager *RoomManagerCtx) containerFilter(ctx context.Context, args dockerFilters.Args) (*dockerContainer.Summary, error) {
args.Add("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", manager.config.InstanceName))
containers, err := manager.client.ContainerList(ctx, dockerTypes.ContainerListOptions{
containers, err := manager.client.ContainerList(ctx, dockerContainer.ListOptions{
All: true,
Filters: args,
})
@@ -74,22 +81,22 @@ func (manager *RoomManagerCtx) containerFilter(ctx context.Context, args filters
return &container, nil
}
func (manager *RoomManagerCtx) containerById(ctx context.Context, id string) (*dockerTypes.Container, error) {
return manager.containerFilter(ctx, filters.NewArgs(
filters.Arg("id", id),
func (manager *RoomManagerCtx) containerById(ctx context.Context, id string) (*dockerContainer.Summary, error) {
return manager.containerFilter(ctx, dockerFilters.NewArgs(
dockerFilters.Arg("id", id),
))
}
func (manager *RoomManagerCtx) containerByName(ctx context.Context, name string) (*dockerTypes.Container, error) {
return manager.containerFilter(ctx, filters.NewArgs(
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.name=%s", name)),
func (manager *RoomManagerCtx) containerByName(ctx context.Context, name string) (*dockerContainer.Summary, error) {
return manager.containerFilter(ctx, dockerFilters.NewArgs(
dockerFilters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.name=%s", name)),
))
}
func (manager *RoomManagerCtx) inspectContainer(ctx context.Context, id string) (*dockerTypes.ContainerJSON, error) {
func (manager *RoomManagerCtx) inspectContainer(ctx context.Context, id string) (*dockerContainer.InspectResponse, error) {
container, err := manager.client.ContainerInspect(ctx, id)
if err != nil {
if dockerClient.IsErrNotFound(err) {
if errdefs.IsNotFound(err) {
return nil, types.ErrRoomNotFound
}
return nil, err
@@ -104,7 +111,7 @@ func (manager *RoomManagerCtx) inspectContainer(ctx context.Context, id string)
}
func (manager *RoomManagerCtx) containerExec(ctx context.Context, id string, cmd []string) (string, error) {
exec, err := manager.client.ContainerExecCreate(ctx, id, dockerTypes.ExecConfig{
exec, err := manager.client.ContainerExecCreate(ctx, id, dockerContainer.ExecOptions{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
@@ -113,13 +120,13 @@ func (manager *RoomManagerCtx) containerExec(ctx context.Context, id string, cmd
Detach: false,
})
if err != nil {
if dockerClient.IsErrNotFound(err) {
if errdefs.IsNotFound(err) {
return "", types.ErrRoomNotFound
}
return "", err
}
conn, err := manager.client.ContainerExecAttach(ctx, exec.ID, dockerTypes.ExecStartCheck{
conn, err := manager.client.ContainerExecAttach(ctx, exec.ID, dockerContainer.ExecAttachOptions{
Detach: false,
Tty: true,
})
+315
View File
@@ -0,0 +1,315 @@
package room
import (
"context"
"fmt"
"io"
"strings"
"sync"
"github.com/m1k1o/neko-rooms/internal/config"
"github.com/m1k1o/neko-rooms/internal/types"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
dockerContainer "github.com/docker/docker/api/types/container"
dockerEvents "github.com/docker/docker/api/types/events"
dockerFilters "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
)
type roomReady struct {
id string
labels map[string]string
}
type events struct {
wg sync.WaitGroup
logger zerolog.Logger
config *config.Room
client *dockerClient.Client
roomsReadyCh chan roomReady
roomsReadyMu sync.Mutex
roomsReady map[string]struct{}
ctx context.Context
cancel context.CancelFunc
listeners []chan types.RoomEvent
listenersMu sync.Mutex
runningRooms prometheus.Gauge
totalRooms prometheus.Counter
}
func newEvents(config *config.Room, client *dockerClient.Client) *events {
return &events{
logger: log.With().Str("module", "events").Logger(),
config: config,
client: client,
roomsReadyCh: make(chan roomReady),
roomsReady: make(map[string]struct{}),
// metrics
runningRooms: promauto.NewGauge(prometheus.GaugeOpts{
Name: "running_rooms",
Namespace: "neko_rooms",
Help: "Number of currently running rooms.",
}),
totalRooms: promauto.NewCounter(prometheus.CounterOpts{
Name: "total_rooms",
Namespace: "neko_rooms",
Help: "Total number of rooms created since start.",
}),
}
}
func (e *events) Start() {
e.ctx, e.cancel = context.WithCancel(context.Background())
// load initial metrics
containers, err := e.client.ContainerList(e.ctx, dockerContainer.ListOptions{
Filters: dockerFilters.NewArgs(
dockerFilters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", e.config.InstanceName)),
),
})
if err != nil {
e.logger.Err(err).Msg("failed to list containers")
return
}
for _, container := range containers {
e.totalRooms.Inc()
if container.State == "running" {
e.runningRooms.Inc()
}
}
msgs, errs := e.client.Events(e.ctx, dockerEvents.ListOptions{
Filters: dockerFilters.NewArgs(
dockerFilters.Arg("type", string(dockerEvents.ContainerEventType)),
dockerFilters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", e.config.InstanceName)),
dockerFilters.Arg("event", string(dockerEvents.ActionCreate)),
dockerFilters.Arg("event", string(dockerEvents.ActionStart)),
dockerFilters.Arg("event", string(dockerEvents.ActionHealthStatus)),
dockerFilters.Arg("event", string(dockerEvents.ActionStop)),
dockerFilters.Arg("event", string(dockerEvents.ActionDestroy)),
dockerFilters.Arg("event", string(dockerEvents.ActionPause)),
dockerFilters.Arg("event", string(dockerEvents.ActionUnPause)),
),
})
e.wg.Add(1)
go func() {
defer e.wg.Done()
e.logger.Info().Msg("docker event listener started")
defer e.logger.Info().Msg("docker event listener stopped")
for {
select {
case <-e.ctx.Done():
e.logger.Info().Msg("docker event context closed")
return
case err, ok := <-errs:
if !ok {
e.logger.Error().Msg("docker event error channel closed")
return
}
e.logger.Err(err).Msg("got docker event error")
return
case room := <-e.roomsReadyCh:
// ignore if room was already ready
if !e.setRoomReady(room.id) {
continue
}
e.broadcast(types.RoomEvent{
ID: room.id,
Action: types.RoomEventReady,
ContainerLabels: room.labels,
})
case msg := <-msgs:
roomId := msg.Actor.ID[:12]
labels := msg.Actor.Attributes
e.logger.Debug().
Str("id", roomId).
Str("action", string(msg.Action)).
Msg("got docker event")
var action types.RoomEventAction
switch msg.Action {
case dockerEvents.ActionCreate:
action = types.RoomEventCreated
e.totalRooms.Inc()
case dockerEvents.ActionStart:
action = types.RoomEventStarted
e.waitForRoomReady(roomId, labels)
e.runningRooms.Inc()
case dockerEvents.ActionHealthStatusHealthy:
action = types.RoomEventReady
// ignore if room was already ready
if !e.setRoomReady(roomId) {
continue
}
case dockerEvents.ActionStop:
action = types.RoomEventStopped
e.setRoomNotReady(roomId)
e.runningRooms.Dec()
case dockerEvents.ActionDestroy:
action = types.RoomEventDestroyed
case dockerEvents.ActionPause:
action = types.RoomEventPaused
e.setRoomNotReady(roomId)
e.runningRooms.Dec()
case dockerEvents.ActionUnPause:
action = types.RoomEventStarted
e.waitForRoomReady(roomId, labels)
e.runningRooms.Inc()
}
e.broadcast(types.RoomEvent{
ID: roomId,
Action: action,
ContainerLabels: labels,
})
}
}
}()
}
func (e *events) Shutdown() error {
e.cancel()
close(e.roomsReadyCh)
e.wg.Wait()
return nil
}
//
// room ready
//
func (e *events) waitForRoomReady(roomId string, labels map[string]string) {
e.wg.Add(1)
go func() {
defer e.wg.Done()
// check if room is ready
exec, err := e.client.ContainerExecCreate(e.ctx, roomId, dockerContainer.ExecOptions{
AttachStdout: true,
Cmd: []string{
"/bin/bash", "-c",
fmt.Sprintf(`for ((a=1; a<=5; a++)); do (echo > /dev/tcp/localhost/%d) >/dev/null && echo -n OK && exit; sleep 1; done; exit`, frontendPort),
},
})
if err != nil {
e.logger.Err(err).Msg("failed to create exec")
return
}
conn, err := e.client.ContainerExecAttach(e.ctx, exec.ID, dockerContainer.ExecAttachOptions{})
if err != nil {
e.logger.Err(err).Msg("failed to attach exec")
return
}
defer conn.Close()
data, err := io.ReadAll(conn.Reader)
if err != nil {
e.logger.Err(err).Msg("failed to read exec")
return
}
if strings.HasSuffix(string(data), "OK") {
e.logger.Debug().Str("id", roomId).Msg("room ready")
e.roomsReadyCh <- roomReady{
id: roomId,
labels: labels,
}
return
}
e.logger.Warn().Str("id", roomId).Str("data", string(data)).Msg("room not ready")
}()
}
func (e *events) setRoomReady(roomId string) bool {
e.roomsReadyMu.Lock()
defer e.roomsReadyMu.Unlock()
_, ok := e.roomsReady[roomId]
e.roomsReady[roomId] = struct{}{}
return !ok
}
func (e *events) setRoomNotReady(roomId string) {
e.roomsReadyMu.Lock()
defer e.roomsReadyMu.Unlock()
delete(e.roomsReady, roomId)
}
func (e *events) IsRoomReady(roomId string) bool {
e.roomsReadyMu.Lock()
defer e.roomsReadyMu.Unlock()
_, ok := e.roomsReady[roomId]
return ok
}
//
// events
//
func (e *events) broadcast(event types.RoomEvent) {
e.listenersMu.Lock()
for _, listener := range e.listeners {
listener <- event
}
e.listenersMu.Unlock()
}
func (e *events) Events(ctx context.Context) (<-chan types.RoomEvent, <-chan error) {
messages := make(chan types.RoomEvent)
errs := make(chan error, 1)
// add listener
e.listenersMu.Lock()
e.listeners = append(e.listeners, messages)
e.listenersMu.Unlock()
e.wg.Add(1)
go func() {
defer e.wg.Done()
defer close(errs)
select {
case <-ctx.Done():
errs <- ctx.Err()
case <-e.ctx.Done():
errs <- fmt.Errorf("room events shutdown")
return
}
// remove listener
e.listenersMu.Lock()
for i, listener := range e.listeners {
if listener == messages {
e.listeners = append(e.listeners[:i], e.listeners[i+1:]...)
break
}
}
e.listenersMu.Unlock()
}()
return messages, errs
}
+2 -2
View File
@@ -121,8 +121,8 @@ 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
if after, ok := strings.CutPrefix(key, "m1k1o.neko_rooms.x-"); ok {
userDefined[after] = val
}
}
+82 -38
View File
@@ -10,16 +10,16 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/docker/cli/opts"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
dockerContainer "github.com/docker/docker/api/types/container"
dockerMount "github.com/docker/docker/api/types/mount"
network "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
dockerNetwork "github.com/docker/docker/api/types/network"
dockerStrslice "github.com/docker/docker/api/types/strslice"
dockerClient "github.com/docker/docker/client"
dockerNames "github.com/docker/docker/daemon/names"
"github.com/docker/go-connections/nat"
@@ -48,6 +48,7 @@ func New(client *dockerClient.Client, config *config.Room) *RoomManagerCtx {
logger: logger,
config: config,
client: client,
events: newEvents(config, client),
}
}
@@ -55,6 +56,7 @@ type RoomManagerCtx struct {
logger zerolog.Logger
config *config.Room
client *dockerClient.Client
events *events
}
func (manager *RoomManagerCtx) Config() types.RoomsConfig {
@@ -244,21 +246,19 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
return "", fmt.Errorf("invalid container name, must match %s", dockerNames.RestrictedNameChars)
}
if in, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoImages); !in {
if !slices.Contains(manager.config.NekoImages, settings.NekoImage) {
return "", fmt.Errorf("invalid neko image")
}
isPrivilegedImage, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoPrivilegedImages)
// if api version is not set, try to detect it
if settings.ApiVersion == 0 {
inspect, _, err := manager.client.ImageInspectWithRaw(ctx, settings.NekoImage)
inspect, err := manager.client.ImageInspect(ctx, settings.NekoImage)
if err != nil {
return "", err
}
// based on image label
if val, ok := inspect.Config.Labels["m1k1o.neko_rooms.api_version"]; ok {
// based on image label (preferred)
if val, ok := inspect.Config.Labels["net.m1k1o.neko.api-version"]; ok {
var err error
settings.ApiVersion, err = strconv.Atoi(val)
if err != nil {
@@ -268,16 +268,18 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// based on opencontainers image url label
if val, ok := inspect.Config.Labels["org.opencontainers.image.url"]; ok {
// TODO: this should be removed in future, but since we have a lot of legacy images, we need to support it
switch val {
case "https://github.com/m1k1o/neko":
case "https://github.com/m1k1o/neko",
"https://github.com/m1k1o/neko-apps":
settings.ApiVersion = 2
case "https://github.com/demodesk/neko":
settings.ApiVersion = 3
}
} else
}
// unable to detect api version
{
// still unable to detect api version
if settings.ApiVersion == 0 {
// TODO: this should be removed in future, but since we have a lot of v2 images, we need to support it
log.Warn().Str("image", settings.NekoImage).Msg("unable to detect api version, fallback to v2")
settings.ApiVersion = 2
@@ -373,11 +375,11 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
traefikRule := "PathPrefix(`" + pathPrefix + "`)"
if t.Domain != "" && t.Domain != "*" {
// match *.domain.tld as subdomain
if strings.HasPrefix(t.Domain, "*.") {
if after, ok := strings.CutPrefix(t.Domain, "*."); ok {
traefikRule = fmt.Sprintf(
"Host(`%s.%s`)",
roomName,
strings.TrimPrefix(t.Domain, "*."),
after,
)
} else {
traefikRule += " && Host(`" + t.Domain + "`)"
@@ -410,12 +412,12 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// add custom labels
for _, label := range manager.config.Labels {
// replace dynamic values in labels
label = strings.Replace(label, "{containerName}", containerName, -1)
label = strings.Replace(label, "{roomName}", roomName, -1)
label = strings.ReplaceAll(label, "{containerName}", containerName)
label = strings.ReplaceAll(label, "{roomName}", roomName)
if t := manager.config.Traefik; t.Enabled {
label = strings.Replace(label, "{traefikEntrypoint}", t.Entrypoint, -1)
label = strings.Replace(label, "{traefikCertresolver}", t.Certresolver, -1)
label = strings.ReplaceAll(label, "{traefikEntrypoint}", t.Entrypoint)
label = strings.ReplaceAll(label, "{traefikCertresolver}", t.Certresolver)
}
v := strings.SplitN(label, "=", 2)
@@ -575,7 +577,7 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// Set container device requests
//
var deviceRequests []container.DeviceRequest
var deviceRequests []dockerContainer.DeviceRequest
if len(settings.Resources.Gpus) > 0 {
gpuOpts := opts.GpuOpts{}
@@ -600,9 +602,9 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// Set container devices
//
var devices []container.DeviceMapping
var devices []dockerContainer.DeviceMapping
for _, device := range settings.Resources.Devices {
devices = append(devices, container.DeviceMapping{
devices = append(devices, dockerContainer.DeviceMapping{
PathOnHost: device,
PathInContainer: device,
CgroupPermissions: "rwm",
@@ -618,7 +620,7 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
hostname = settings.Hostname
}
config := &container.Config{
config := &dockerContainer.Config{
// Hostname
Hostname: hostname,
// Domainname is preventing from running container on LXC (Proxmox)
@@ -634,20 +636,20 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
Labels: labels,
}
hostConfig := &container.HostConfig{
hostConfig := &dockerContainer.HostConfig{
// Port mapping between the exposed port (container) and the host
PortBindings: portBindings,
// Configuration of the logs for this container
LogConfig: container.LogConfig{
LogConfig: dockerContainer.LogConfig{
Type: "json-file",
Config: map[string]string{},
},
// Restart policy to be used for the container
RestartPolicy: container.RestartPolicy{
RestartPolicy: dockerContainer.RestartPolicy{
Name: "unless-stopped",
},
// List of kernel capabilities to add to the container
CapAdd: strslice.StrSlice{
CapAdd: dockerStrslice.StrSlice{
"SYS_ADMIN",
},
// Total shm memory usage
@@ -655,7 +657,7 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// Mounts specs used by the container
Mounts: mounts,
// Resources contains container's resources (cgroups config, ulimits...)
Resources: container.Resources{
Resources: dockerContainer.Resources{
CPUShares: settings.Resources.CPUShares,
NanoCPUs: settings.Resources.NanoCPUs,
Memory: settings.Resources.Memory,
@@ -665,11 +667,11 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// DNS
DNS: settings.DNS,
// Privileged
Privileged: isPrivilegedImage,
Privileged: slices.Contains(manager.config.NekoPrivilegedImages, settings.NekoImage),
}
networkingConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
networkingConfig := &dockerNetwork.NetworkingConfig{
EndpointsConfig: map[string]*dockerNetwork.EndpointSettings{
manager.config.InstanceNetwork: {},
},
}
@@ -722,14 +724,17 @@ func (manager *RoomManagerCtx) Remove(ctx context.Context, id string) error {
}
// Stop the actual container
err = manager.client.ContainerStop(ctx, id, container.StopOptions{})
err = manager.client.ContainerStop(ctx, id, dockerContainer.StopOptions{
Signal: "SIGTERM",
Timeout: &manager.config.StopTimeoutSec,
})
if err != nil {
return err
}
// Remove the actual container
err = manager.client.ContainerRemove(ctx, id, dockerTypes.ContainerRemoveOptions{
err = manager.client.ContainerRemove(ctx, id, dockerContainer.RemoveOptions{
RemoveVolumes: true,
Force: true,
})
@@ -980,13 +985,22 @@ func (manager *RoomManagerCtx) GetStats(ctx context.Context, id string) (*types.
}
func (manager *RoomManagerCtx) Start(ctx context.Context, id string) error {
_, err := manager.inspectContainer(ctx, id)
container, err := manager.inspectContainer(ctx, id)
if err != nil {
return err
}
// If paused, we need to unpause the container
if container.State.Paused {
if err := manager.client.ContainerUnpause(ctx, id); err != nil {
return err
}
return nil
}
// Start the actual container
return manager.client.ContainerStart(ctx, id, dockerTypes.ContainerStartOptions{})
return manager.client.ContainerStart(ctx, id, dockerContainer.StartOptions{})
}
func (manager *RoomManagerCtx) Stop(ctx context.Context, id string) error {
@@ -996,7 +1010,10 @@ func (manager *RoomManagerCtx) Stop(ctx context.Context, id string) error {
}
// Stop the actual container
return manager.client.ContainerStop(ctx, id, container.StopOptions{})
return manager.client.ContainerStop(ctx, id, dockerContainer.StopOptions{
Signal: "SIGTERM",
Timeout: &manager.config.StopTimeoutSec,
})
}
func (manager *RoomManagerCtx) Restart(ctx context.Context, id string) error {
@@ -1006,5 +1023,32 @@ func (manager *RoomManagerCtx) Restart(ctx context.Context, id string) error {
}
// Restart the actual container
return manager.client.ContainerRestart(ctx, id, container.StopOptions{})
return manager.client.ContainerRestart(ctx, id, dockerContainer.StopOptions{
Signal: "SIGTERM",
Timeout: &manager.config.StopTimeoutSec,
})
}
func (manager *RoomManagerCtx) Pause(ctx context.Context, id string) error {
_, err := manager.inspectContainer(ctx, id)
if err != nil {
return err
}
// Pause the actual container
return manager.client.ContainerPause(ctx, id)
}
// events
func (manager *RoomManagerCtx) EventsLoopStart() {
manager.events.Start()
}
func (manager *RoomManagerCtx) EventsLoopStop() error {
return manager.events.Shutdown()
}
func (manager *RoomManagerCtx) Events(ctx context.Context) (<-chan types.RoomEvent, <-chan error) {
return manager.events.Events(ctx)
}
-33
View File
@@ -1,33 +0,0 @@
package server
import (
"net/http"
"net/http/pprof"
"github.com/go-chi/chi/v5"
)
const pprofPath = "/debug/pprof/"
func withPProf(router *chi.Mux) {
router.Route(pprofPath, func(r chi.Router) {
r.Get("/", pprof.Index)
r.Get("/{action}", func(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
switch action {
case "cmdline":
pprof.Cmdline(w, r)
case "profile":
pprof.Profile(w, r)
case "symbol":
pprof.Symbol(w, r)
case "trace":
pprof.Trace(w, r)
default:
pprof.Handler(action).ServeHTTP(w, r)
}
})
})
}
+8 -8
View File
@@ -14,7 +14,7 @@ type logformatter struct {
}
func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
req := map[string]interface{}{}
req := map[string]any{}
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
req["id"] = reqID
@@ -32,7 +32,7 @@ func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
req["agent"] = r.UserAgent()
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
fields := map[string]interface{}{}
fields := map[string]any{}
fields["req"] = req
return &logentry{
@@ -43,12 +43,12 @@ func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
type logentry struct {
logger zerolog.Logger
fields map[string]interface{}
errors []map[string]interface{}
fields map[string]any
errors []map[string]any
}
func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
res := map[string]interface{}{}
func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) {
res := map[string]any{}
res["time"] = time.Now().UTC().Format(time.RFC1123)
res["status"] = status
res["bytes"] = bytes
@@ -65,8 +65,8 @@ func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Dur
}
}
func (e *logentry) Panic(v interface{}, stack []byte) {
err := map[string]interface{}{}
func (e *logentry) Panic(v any, stack []byte) {
err := map[string]any{}
err["message"] = fmt.Sprintf("%+v", v)
err["stack"] = string(stack)
+14 -6
View File
@@ -15,6 +15,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -58,12 +60,6 @@ func New(ApiManager types.ApiManager, roomConfig *config.Room, config *config.Se
}))
}
// mount pprof endpoint
if config.PProf {
withPProf(router)
logger.Info().Msgf("with pprof endpoint at %s", pprofPath)
}
//
// admin page
//
@@ -240,6 +236,18 @@ func New(ApiManager types.ApiManager, roomConfig *config.Room, config *config.Se
})
}
// mount pprof endpoint
if config.PProf {
router.Mount("/debug", middleware.Profiler())
logger.Info().Msgf("with pprof endpoint")
}
// mount metrics prometheus endpoint
if config.Metrics {
router.Mount("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}))
logger.Info().Msgf("with metrics endpoint")
}
// handle all remaining paths with proxy
router.Handle("/*", proxyHandler)
+27
View File
@@ -23,9 +23,13 @@ type RoomEntry struct {
IsOutdated bool `json:"is_outdated"`
MaxConnections uint16 `json:"max_connections"` // 0 when using mux
Running bool `json:"running"`
Paused bool `json:"paused"`
IsReady bool `json:"is_ready"`
Status string `json:"status"`
Created time.Time `json:"created"`
Labels map[string]string `json:"labels,omitempty"`
ContainerLabels map[string]string `json:"-"` // for internal use
}
type MountType string
@@ -138,6 +142,24 @@ type RoomMember struct {
Muted bool `json:"muted"`
}
type RoomEventAction string
const (
RoomEventCreated RoomEventAction = "created"
RoomEventStarted RoomEventAction = "started"
RoomEventReady RoomEventAction = "ready"
RoomEventStopped RoomEventAction = "stopped"
RoomEventDestroyed RoomEventAction = "destroyed"
RoomEventPaused RoomEventAction = "paused"
)
type RoomEvent struct {
ID string `json:"id"`
Action RoomEventAction `json:"action"`
ContainerLabels map[string]string `json:"-"` // for internal use
}
var ErrRoomNotFound = fmt.Errorf("room not found")
type RoomManager interface {
@@ -155,4 +177,9 @@ type RoomManager interface {
Start(ctx context.Context, id string) error
Stop(ctx context.Context, id string) error
Restart(ctx context.Context, id string) error
Pause(ctx context.Context, id string) error
EventsLoopStart()
EventsLoopStop() error
Events(ctx context.Context) (<-chan RoomEvent, <-chan error)
}
+5 -9
View File
@@ -2,11 +2,11 @@ package types
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/m1k1o/neko-rooms/internal/config"
"github.com/m1k1o/neko-rooms/internal/utils"
)
//
@@ -98,7 +98,7 @@ func (settings *RoomSettings) toEnvV2(config *config.Room, ports PortSettings) [
}
for key, val := range settings.Envs {
if in, _ := utils.ArrayIn(key, blacklistedEnvsV2); !in {
if !slices.Contains(blacklistedEnvsV2, key) {
env = append(env, fmt.Sprintf("%s=%s", key, val))
}
}
@@ -122,13 +122,9 @@ func (settings *RoomSettings) fromEnvV2(envs []string) error {
case "NEKO_PASSWORD_ADMIN":
settings.AdminPass = val
case "NEKO_CONTROL_PROTECTION":
if ok, _ := strconv.ParseBool(val); ok {
settings.ControlProtection = true
}
settings.ControlProtection, err = strconv.ParseBool(val)
case "NEKO_IMPLICIT_CONTROL":
if ok, _ := strconv.ParseBool(val); ok {
settings.ImplicitControl = true
}
settings.ImplicitControl, err = strconv.ParseBool(val)
case "NEKO_SCREEN":
settings.Screen = val
case "NEKO_MAX_FPS":
@@ -148,7 +144,7 @@ func (settings *RoomSettings) fromEnvV2(envs []string) error {
case "NEKO_AUDIO":
settings.AudioPipeline = val
default:
if in, _ := utils.ArrayIn(key, blacklistedEnvsV2); !in {
if !slices.Contains(blacklistedEnvsV2, key) {
settings.Envs[key] = val
}
}
+15 -16
View File
@@ -2,11 +2,11 @@ package types
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/m1k1o/neko-rooms/internal/config"
"github.com/m1k1o/neko-rooms/internal/utils"
)
//
@@ -69,13 +69,16 @@ func (settings *RoomSettings) toEnvV3(config *config.Room, ports PortSettings) [
env = append(env, fmt.Sprintf("NEKO_WEBRTC_NAT1TO1=%s", strings.Join(config.NAT1To1IPs, ",")))
}
//if settings.ControlProtection {
// env = append(env, "NEKO_CONTROL_PROTECTION=true") // TODO: not supported yet
//}
if settings.ControlProtection {
env = append(env, "NEKO_SESSION_CONTROL_PROTECTION=true")
}
// implicit control - enabled by default
// implicit control - enabled by default but in legacy mode disabled by default
// so we need to set it explicitly until legacy mode is removed
if !settings.ImplicitControl {
env = append(env, "NEKO_SESSION_IMPLICIT_HOSTING=false")
} else {
env = append(env, "NEKO_SESSION_IMPLICIT_HOSTING=true")
}
if settings.VideoCodec != "VP8" { // VP8 is default
@@ -87,7 +90,7 @@ func (settings *RoomSettings) toEnvV3(config *config.Room, ports PortSettings) [
//}
if settings.VideoPipeline != "" {
env = append(env, fmt.Sprintf("NEKO_CAPTURE_VIDEO_PIPELINES=%s", settings.VideoPipeline)) // TOOD: multiple pipelines, as JSON
env = append(env, fmt.Sprintf("NEKO_CAPTURE_VIDEO_PIPELINE=%s", settings.VideoPipeline)) // TOOD: allow simulcast pipelines
}
if settings.AudioCodec != "OPUS" { // OPUS is default
@@ -107,7 +110,7 @@ func (settings *RoomSettings) toEnvV3(config *config.Room, ports PortSettings) [
}
for key, val := range settings.Envs {
if in, _ := utils.ArrayIn(key, blacklistedEnvsV3); !in {
if !slices.Contains(blacklistedEnvsV3, key) {
env = append(env, fmt.Sprintf("%s=%s", key, val))
}
}
@@ -132,14 +135,10 @@ func (settings *RoomSettings) fromEnvV3(envs []string) error {
settings.UserPass = val
case "NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD":
settings.AdminPass = val
//case "NEKO_CONTROL_PROTECTION": // TODO: not supported yet
// if ok, _ := strconv.ParseBool(val); ok {
// settings.ControlProtection = true
// }
case "NEKO_SESSION_CONTROL_PROTECTION":
settings.ControlProtection, err = strconv.ParseBool(val)
case "NEKO_SESSION_IMPLICIT_HOSTING":
if ok, _ := strconv.ParseBool(val); !ok {
settings.ImplicitControl = false
}
settings.ImplicitControl, err = strconv.ParseBool(val)
case "NEKO_DESKTOP_SCREEN":
settings.Screen = val
//case "NEKO_MAX_FPS": // TODO: not supported yet
@@ -150,7 +149,7 @@ func (settings *RoomSettings) fromEnvV3(envs []string) error {
settings.VideoCodec = strings.ToUpper(val)
//case "NEKO_VIDEO_BITRATE": // TODO: not supported yet
// settings.VideoBitrate, err = strconv.Atoi(val)
case "NEKO_CAPTURE_VIDEO_PIPELINES": // TOOD: multiple pipelines, as JSON
case "NEKO_CAPTURE_VIDEO_PIPELINE": // TOOD: allow simulcast pipelines
settings.VideoPipeline = val
case "NEKO_CAPTURE_AUDIO_CODEC":
settings.AudioCodec = strings.ToUpper(val)
@@ -159,7 +158,7 @@ func (settings *RoomSettings) fromEnvV3(envs []string) error {
case "NEKO_CAPTURE_AUDIO_PIPELINE":
settings.AudioPipeline = val
default:
if in, _ := utils.ArrayIn(key, blacklistedEnvsV3); !in {
if !slices.Contains(blacklistedEnvsV3, key) {
settings.Envs[key] = val
}
}
-14
View File
@@ -1,14 +0,0 @@
package utils
func ArrayIn[T comparable](val T, array []T) (exists bool, index int) {
exists, index = false, -1
for i, a := range array {
if a == val {
exists, index = true, i
return
}
}
return
}
+1 -1
View File
@@ -29,6 +29,6 @@ func Color(str string) string {
return result + str[lastIndex:]
}
func Colorf(format string, a ...interface{}) string {
func Colorf(format string, a ...any) string {
return fmt.Sprintf(Color(format), a...)
}
+1 -1
View File
@@ -17,7 +17,7 @@ func Swal2Response(w http.ResponseWriter, body string) {
http.Error(w, err.Error(), 500)
}
err = tmpl.Execute(w, map[string]interface{}{
err = tmpl.Execute(w, map[string]any{
"Body": template.HTML(body),
})
+1 -1
View File
@@ -34,7 +34,7 @@ func initMasks(params ...int) []uint {
}
func getMask(alphabet string, masks []uint) int {
for i := 0; i < len(masks); i++ {
for i := range masks {
curr := int(masks[i])
if curr >= len(alphabet)-1 {
return curr
+12 -4
View File
@@ -120,17 +120,23 @@ func (main *MainCtx) Preflight() {
}
func (main *MainCtx) Start() {
client, err := client.NewClientWithOpts(client.FromEnv)
client, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
if err != nil {
main.logger.Panic().Err(err).Msg("unable to connect to docker client")
} else {
main.logger.Info().Msg("successfully connected to docker client")
main.logger.Info().
Str("version", client.ClientVersion()).
Msg("successfully connected to docker client (API negotiation enabled)")
}
main.roomManager = room.New(
client,
main.Configs.Room,
)
main.roomManager.EventsLoopStart()
main.pullManager = pull.New(
client,
@@ -143,8 +149,7 @@ func (main *MainCtx) Start() {
)
main.proxyManager = proxy.New(
client,
main.Configs.Room.InstanceName,
main.roomManager,
main.Configs.Room.WaitEnabled,
)
main.proxyManager.Start()
@@ -169,6 +174,9 @@ func (main *MainCtx) Shutdown() {
err = main.pullManager.Shutdown()
main.logger.Err(err).Msg("pull manager shutdown")
err = main.roomManager.EventsLoopStop()
main.logger.Err(err).Msg("room events loop shutdown")
}
func (main *MainCtx) ServeCommand(cmd *cobra.Command, args []string) {
+1 -1
View File
@@ -7,7 +7,7 @@ networks:
services:
traefik:
image: "traefik:2.4"
image: "traefik:2"
restart: "unless-stopped"
environment:
- "TZ=Europe/Vienna"
+1 -1
View File
@@ -12,7 +12,7 @@ networks:
services:
traefik:
image: "traefik:2.4"
image: "traefik:2"
restart: "unless-stopped"
environment:
- "TZ"
+26 -21
View File
@@ -2,13 +2,14 @@
echo "
---------------------------------------------------------------
__
____ ___ / /______ _________ ____ ____ ___ _____
/ __ \/ _ \/ //_/ __ \ / ___/ __ \/ __ \/ __ '__ \/ ___/
/ / / / __/ ,< / /_/ /_____/ / / /_/ / /_/ / / / / / (__ )
/_/ /_/\___/_/|_|\____/_____/_/ \____/\____/_/ /_/ /_/____/
Automatic installer by m1k1o
____ ___ / /______ _________ ____ ____ ___ _____
/ __ \/ _ \/ //_/ __ \ / ___/ __ \/ __ \/ __ '__ \/ ___/
/ / / / __/ ,< / /_/ /_____/ / / /_/ / /_/ / / / / / (__ )
/_/ /_/\___/_/|_|\____/_____/_/ \____/\____/_/ /_/ /_/____/
Automatic installer by m1k1o
---------------------------------------------------------------
You need to have:
@@ -113,7 +114,7 @@ if ! dockerd --help > /dev/null 2>&1; then
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y containerd.io docker-ce docker-ce-cli
apt-get install -y containerd.io docker-ce docker-ce-cli docker-buildx-plugin docker-compose-plugin
fi
echo "[Y] Docker is installed..."
@@ -123,7 +124,7 @@ echo "[Y] Docker is installed..."
#
apt-get update
apt-get install -y apache2-utils docker-compose sed
apt-get install -y apache2-utils sed
echo "[Y] Dependencies are installed..."
@@ -166,6 +167,12 @@ while true; do
break
done
PUBLIC_IP=$(curl -s https://ipinfo.io/ip)
read -rp "Enter your server's public IP address (autodetected: ${PUBLIC_IP}): " NEKO_NAT1TO1
if [[ -z "${NEKO_NAT1TO1}" ]]; then
NEKO_NAT1TO1="${PUBLIC_IP}"
fi
touch "./usersfile"
# Users
@@ -202,8 +209,6 @@ echo "[Y] Got all settings..."
# Create env
#
NEKO_IMAGES=(m1k1o/neko:firefox m1k1o/neko:chromium m1k1o/neko:google-chrome)
{
echo "TZ=${TZ}"
echo "NEKO_ROOMS_EPR=${NEKO_ROOMS_EPR}"
@@ -211,7 +216,7 @@ NEKO_IMAGES=(m1k1o/neko:firefox m1k1o/neko:chromium m1k1o/neko:google-chrome)
echo "NEKO_ROOMS_TRAEFIK_ENTRYPOINT=websecure"
echo "NEKO_ROOMS_TRAEFIK_NETWORK=neko-rooms-traefik"
echo "NEKO_ROOMS_TRAEFIK_CERTRESOLVER=lets-encrypt"
echo "NEKO_ROOMS_NEKO_IMAGES=${NEKO_IMAGES[*]}"
echo "NEKO_NAT1TO1=${NEKO_NAT1TO1}"
} > .env
echo "[Y] Creating env..."
@@ -240,13 +245,13 @@ echo "[Y] Downloading traefik config..."
wget -O "./docker-compose.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/docker-compose.yml"
# Pull neko images
for NEKO_IMAGE in "${NEKO_IMAGES[@]}"; do
docker pull "${NEKO_IMAGE}"
done
# docker-compose was renamed to docker compose, support both
if docker-compose --version > /dev/null 2>&1; then
docker-compose pull
docker-compose up -d
else
docker compose pull
docker compose up -d
fi
# Start
docker-compose pull
docker-compose up -d
echo "[Y] Finished! You can now visit https://${NEKO_ROOMS_TRAEFIK_DOMAIN}/"
echo "[Y] Finished! You can now visit https://${NEKO_ROOMS_TRAEFIK_DOMAIN}/"
+1 -1
View File
@@ -7,7 +7,7 @@ networks:
services:
traefik: # domain name used in nginx config
image: "traefik:2.4"
image: "traefik:2"
restart: "unless-stopped"
environment:
- "TZ=Europe/Vienna"
+1 -1
View File
@@ -7,7 +7,7 @@ networks:
services:
traefik: # domain name used in nginx config
image: "traefik:2.4"
image: "traefik:2"
restart: "unless-stopped"
environment:
- "TZ=Europe/Vienna"