mirror of
https://github.com/m1k1o/neko-rooms.git
synced 2026-11-02 22:51:34 -06:00
pull images.
This commit is contained in:
+97
@@ -221,6 +221,51 @@ paths:
|
||||
'500':
|
||||
description: Internal server error
|
||||
|
||||
/api/pull:
|
||||
get:
|
||||
tags:
|
||||
- default
|
||||
summary: Get pull status
|
||||
operationId: pullStatus
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PullStatus'
|
||||
'500':
|
||||
description: Internal server error
|
||||
post:
|
||||
tags:
|
||||
- default
|
||||
summary: Start new pull
|
||||
operationId: pullStart
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PullStart'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PullStatus'
|
||||
'500':
|
||||
description: Internal server error
|
||||
delete:
|
||||
tags:
|
||||
- default
|
||||
summary: Stop existing pull in progress
|
||||
operationId: pullStop
|
||||
responses:
|
||||
'204':
|
||||
description: OK
|
||||
'500':
|
||||
description: Internal server error
|
||||
|
||||
components:
|
||||
schemas:
|
||||
RoomsConfig:
|
||||
@@ -462,3 +507,55 @@ components:
|
||||
url:
|
||||
type: string
|
||||
example: https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi
|
||||
|
||||
PullStart:
|
||||
type: object
|
||||
properties:
|
||||
neko_image:
|
||||
type: string
|
||||
example: m1k1o/neko:latest
|
||||
registry_user:
|
||||
type: string
|
||||
registry_pass:
|
||||
type: string
|
||||
|
||||
PullLayer:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
example: m1k1o/neko:latest
|
||||
progressDetail:
|
||||
type: object
|
||||
properties:
|
||||
current:
|
||||
type: number
|
||||
total:
|
||||
type: number
|
||||
progress:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
|
||||
PullStatus:
|
||||
type: object
|
||||
properties:
|
||||
active:
|
||||
type: boolean
|
||||
example: true
|
||||
started:
|
||||
type: string
|
||||
format: datetime
|
||||
example: "2021-03-07T21:56:34Z"
|
||||
layers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/PullLayer'
|
||||
status:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
finished:
|
||||
type: string
|
||||
format: datetime
|
||||
example: "2021-03-07T21:56:34Z"
|
||||
|
||||
@@ -100,6 +100,118 @@ export interface BrowserPolicyExtension {
|
||||
*/
|
||||
'url'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PullLayer
|
||||
*/
|
||||
export interface PullLayer {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullLayer
|
||||
*/
|
||||
'status'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {PullLayerProgressDetail}
|
||||
* @memberof PullLayer
|
||||
*/
|
||||
'progressDetail'?: PullLayerProgressDetail;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullLayer
|
||||
*/
|
||||
'progress'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullLayer
|
||||
*/
|
||||
'id'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PullLayerProgressDetail
|
||||
*/
|
||||
export interface PullLayerProgressDetail {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof PullLayerProgressDetail
|
||||
*/
|
||||
'current'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof PullLayerProgressDetail
|
||||
*/
|
||||
'total'?: number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PullStart
|
||||
*/
|
||||
export interface PullStart {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullStart
|
||||
*/
|
||||
'neko_image'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullStart
|
||||
*/
|
||||
'registry_user'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullStart
|
||||
*/
|
||||
'registry_pass'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PullStatus
|
||||
*/
|
||||
export interface PullStatus {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof PullStatus
|
||||
*/
|
||||
'active'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullStatus
|
||||
*/
|
||||
'started'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {{ [key: string]: PullLayer; }}
|
||||
* @memberof PullStatus
|
||||
*/
|
||||
'layers'?: { [key: string]: PullLayer; };
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof PullStatus
|
||||
*/
|
||||
'status'?: Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PullStatus
|
||||
*/
|
||||
'finished'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -577,6 +689,231 @@ export class ConfigApi extends BaseAPI {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DefaultApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary Start new pull
|
||||
* @param {PullStart} [pullStart]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
pullStart: async (pullStart?: PullStart, options: AxiosRequestConfig = {}): 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);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(pullStart, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get pull status
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
pullStatus: async (options: AxiosRequestConfig = {}): 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);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...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 Stop existing pull in progress
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
pullStop: async (options: AxiosRequestConfig = {}): 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);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...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,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DefaultApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary Start new pull
|
||||
* @param {PullStart} [pullStart]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async pullStart(pullStart?: PullStart, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PullStatus>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStart(pullStart, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get pull status
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async pullStatus(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PullStatus>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStatus(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Stop existing pull in progress
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async pullStop(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.pullStop(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DefaultApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = DefaultApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @summary Start new pull
|
||||
* @param {PullStart} [pullStart]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
pullStart(pullStart?: PullStart, options?: any): AxiosPromise<PullStatus> {
|
||||
return localVarFp.pullStart(pullStart, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Get pull status
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
pullStatus(options?: any): AxiosPromise<PullStatus> {
|
||||
return localVarFp.pullStatus(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Stop existing pull in progress
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
pullStop(options?: any): AxiosPromise<void> {
|
||||
return localVarFp.pullStop(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* DefaultApi - object-oriented interface
|
||||
* @export
|
||||
* @class DefaultApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class DefaultApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @summary Start new pull
|
||||
* @param {PullStart} [pullStart]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public pullStart(pullStart?: PullStart, options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).pullStart(pullStart, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get pull status
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public pullStatus(options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).pullStatus(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Stop existing pull in progress
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof DefaultApi
|
||||
*/
|
||||
public pullStop(options?: AxiosRequestConfig) {
|
||||
return DefaultApiFp(this.configuration).pullStop(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RoomsApi - axios parameter creator
|
||||
* @export
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<span>
|
||||
<v-btn
|
||||
@click="dialog = true"
|
||||
:loading="loading"
|
||||
color="info"
|
||||
dark
|
||||
>
|
||||
<v-icon class="mr-2" color="white">mdi-cloud-download-outline</v-icon> Pull neko images
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="920px">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Pull neko images
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<template v-if="status.layers.length > 0">
|
||||
<pre v-for="layer in status.layers" :key="layer.id">{{ layer.id }} {{ layer.progress && layer.progress + ' ' }}{{ layer.status }}</pre>
|
||||
<br />
|
||||
</template>
|
||||
<template v-if="status.status.length > 0">
|
||||
<pre v-for="text in status.status" :key="text">{{ text }}</pre>
|
||||
</template>
|
||||
<pre v-else>Preparing docker image pull</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<template v-if="!status.active">
|
||||
<v-select
|
||||
v-model="nekoImage"
|
||||
:items="nekoImages"
|
||||
dense
|
||||
outlined
|
||||
hide-details
|
||||
label="Neko image"
|
||||
></v-select>
|
||||
<v-btn color="green" class="ml-2" :loading="loading" @click="Start">
|
||||
Start
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="status.active" color="red" text :loading="loading" @click="Stop">
|
||||
Stop
|
||||
</v-btn>
|
||||
<v-btn v-else color="grey" text @click="dialog = false">
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
export default class PUll extends Vue {
|
||||
private dialog = false
|
||||
private loading = false
|
||||
private nekoImage = ''
|
||||
|
||||
get status() {
|
||||
return this.$store.state.pullStatus
|
||||
}
|
||||
|
||||
get nekoImages() {
|
||||
return this.$store.state.roomsConfig.neko_images
|
||||
}
|
||||
|
||||
async Start() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('PULL_START', this.nekoImage)
|
||||
} catch(e) {
|
||||
if (e.response) {
|
||||
this.$swal({
|
||||
title: 'Server error',
|
||||
text: e.response.data,
|
||||
icon: 'error',
|
||||
})
|
||||
} else {
|
||||
this.$swal({
|
||||
title: 'Network error',
|
||||
text: String(e),
|
||||
icon: 'error',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async Stop() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('PULL_STOP')
|
||||
} catch(e) {
|
||||
if (e.response) {
|
||||
this.$swal({
|
||||
title: 'Server error',
|
||||
text: e.response.data,
|
||||
icon: 'error',
|
||||
})
|
||||
} else {
|
||||
this.$swal({
|
||||
title: 'Network error',
|
||||
text: String(e),
|
||||
icon: 'error',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.dialog = false
|
||||
}
|
||||
}
|
||||
|
||||
private interval!: number
|
||||
|
||||
async mounted() {
|
||||
await this.$store.dispatch('PULL_STATUS')
|
||||
|
||||
this.interval = window.setInterval(async () => {
|
||||
if (!this.status.active) return
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('PULL_STATUS')
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
window.clearInterval(this.interval)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
RoomSettings,
|
||||
RoomStats,
|
||||
RoomsApi,
|
||||
DefaultApi,
|
||||
PullStatus,
|
||||
} from '@/api/index'
|
||||
|
||||
import { state, State } from './state'
|
||||
@@ -21,6 +23,7 @@ const configuration = new Configuration({
|
||||
|
||||
const configApi = new ConfigApi(configuration)
|
||||
const roomsApi = new RoomsApi(configuration)
|
||||
const defaultApi = new DefaultApi(configuration)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state,
|
||||
@@ -55,6 +58,9 @@ export default new Vuex.Store({
|
||||
const roomEntries = state.rooms.filter(({ id }) => id != roomId)
|
||||
Vue.set(state, 'rooms', roomEntries)
|
||||
},
|
||||
PULL_STATUS(state: State, pullStatus: PullStatus) {
|
||||
Vue.set(state, 'pullStatus', pullStatus)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async ROOMS_CONFIG({ commit }: ActionContext<State, State>) {
|
||||
@@ -112,6 +118,23 @@ export default new Vuex.Store({
|
||||
commit('ROOMS_PUT', res.data)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async PULL_START({ commit }: ActionContext<State, State>, nekoImage: string) {
|
||||
const res = await defaultApi.pullStart({
|
||||
// eslint-disable-next-line
|
||||
neko_image: nekoImage,
|
||||
})
|
||||
commit('PULL_STATUS', res.data)
|
||||
return res.data
|
||||
},
|
||||
async PULL_STATUS({ commit }: ActionContext<State, State>) {
|
||||
const res = await defaultApi.pullStatus()
|
||||
commit('PULL_STATUS', res.data)
|
||||
},
|
||||
async PULL_STOP() {
|
||||
const res = await defaultApi.pullStop()
|
||||
return res.data
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BrowserPolicyContent,
|
||||
BrowserPolicyTypeEnum,
|
||||
BrowserPolicyExtension,
|
||||
PullStatus,
|
||||
} from '@/api/index'
|
||||
|
||||
export interface BrowserPolicyConfig {
|
||||
@@ -208,6 +209,13 @@ export const state = {
|
||||
// eslint-disable-next-line
|
||||
persistent_data: false,
|
||||
} as BrowserPolicyContent,
|
||||
|
||||
//
|
||||
// default
|
||||
//
|
||||
pullStatus: {
|
||||
active: false,
|
||||
} as PullStatus
|
||||
}
|
||||
|
||||
export type State = typeof state
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
</v-row>
|
||||
|
||||
<RoomsList :loading="loading" />
|
||||
|
||||
<div class="mt-5 text-center">
|
||||
<Pull />
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -66,6 +70,7 @@ import { Component, Vue, Watch } from 'vue-property-decorator'
|
||||
import RoomsList from '@/components/RoomsList.vue'
|
||||
import RoomsQuick from '@/components/RoomsQuick.vue'
|
||||
import RoomsCreate from '@/components/RoomsCreate.vue'
|
||||
import Pull from '@/components/Pull.vue'
|
||||
import { RoomEntry } from '@/api/index'
|
||||
|
||||
@Component({
|
||||
@@ -73,6 +78,7 @@ import { RoomEntry } from '@/api/index'
|
||||
RoomsList,
|
||||
RoomsQuick,
|
||||
RoomsCreate,
|
||||
Pull,
|
||||
}
|
||||
})
|
||||
export default class Home extends Vue {
|
||||
|
||||
@@ -41,4 +41,10 @@ func (manager *ApiManagerCtx) Mount(r chi.Router) {
|
||||
r.Post("/restart", manager.roomGenericAction(manager.rooms.Restart))
|
||||
r.Post("/recreate", manager.roomRecreate)
|
||||
})
|
||||
|
||||
r.Route("/pull", func(r chi.Router) {
|
||||
r.Get("/", manager.roomPullStatus)
|
||||
r.Post("/", manager.roomPullStart)
|
||||
r.Delete("/", manager.roomPullStop)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,3 +169,38 @@ func (manager *ApiManagerCtx) roomGenericAction(action func(id string) error) fu
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomPullStart(w http.ResponseWriter, r *http.Request) {
|
||||
request := types.PullStart{}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
err := manager.rooms.PullStart(request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
response := manager.rooms.PullStatus()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomPullStatus(w http.ResponseWriter, r *http.Request) {
|
||||
response := manager.rooms.PullStatus()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomPullStop(w http.ResponseWriter, r *http.Request) {
|
||||
err := manager.rooms.PullStop()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ type RoomManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
config *config.Room
|
||||
client *dockerClient.Client
|
||||
pull PullConfig
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Config() types.RoomsConfig {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package room
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"m1k1o/neko_rooms/internal/utils"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
type PullConfig struct {
|
||||
mu sync.Mutex
|
||||
cancel func()
|
||||
status types.PullStatus
|
||||
}
|
||||
|
||||
func (pull *PullConfig) TryInitialize(cancel func()) bool {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
if pull.status.Active {
|
||||
cancel()
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pull.cancel = cancel
|
||||
|
||||
pull.status = types.PullStatus{
|
||||
Active: true,
|
||||
Started: &now,
|
||||
Layers: map[string]types.PullLayer{},
|
||||
Status: []string{},
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (pull *PullConfig) SetDone() {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
pull.status.Active = false
|
||||
pull.status.Finished = &now
|
||||
}
|
||||
|
||||
func (pull *PullConfig) Stop() bool {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
if pull.status.Active {
|
||||
pull.cancel()
|
||||
}
|
||||
|
||||
return pull.status.Active
|
||||
}
|
||||
|
||||
func (pull *PullConfig) Get() types.PullStatus {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
return pull.status
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) PullStart(request types.PullStart) error {
|
||||
if in, _ := utils.ArrayIn(request.NekoImage, manager.config.NekoImages); !in {
|
||||
return fmt.Errorf("unknown neko image")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if !manager.pull.TryInitialize(cancel) {
|
||||
return fmt.Errorf("pull is already in progess")
|
||||
}
|
||||
|
||||
// handle registry auth
|
||||
var opts dockerTypes.ImagePullOptions
|
||||
if request.RegistryUser != "" && request.RegistryPass != "" {
|
||||
authConfig := dockerTypes.AuthConfig{
|
||||
Username: request.RegistryUser,
|
||||
Password: request.RegistryPass,
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
opts = dockerTypes.ImagePullOptions{
|
||||
RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := manager.client.ImagePull(ctx, request.NekoImage, opts)
|
||||
|
||||
if err != nil {
|
||||
manager.pull.SetDone()
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
data := scanner.Bytes()
|
||||
|
||||
layer := types.PullLayer{}
|
||||
if err := json.Unmarshal(data, &layer); err != nil {
|
||||
manager.pull.status.Status = append(
|
||||
manager.pull.status.Status,
|
||||
fmt.Sprintf("Error while parsing pull response: %s", err),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if layer.ProgressDetail != nil {
|
||||
manager.pull.status.Layers[layer.ID] = layer
|
||||
} else {
|
||||
manager.pull.status.Status = append(
|
||||
manager.pull.status.Status,
|
||||
layer.Status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
manager.pull.status.Status = append(
|
||||
manager.pull.status.Status,
|
||||
fmt.Sprintf("Error while reading pull response: %s", err),
|
||||
)
|
||||
}
|
||||
|
||||
reader.Close()
|
||||
manager.pull.SetDone()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) PullStop() error {
|
||||
if !manager.pull.Stop() {
|
||||
return fmt.Errorf("pull is not in progess")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) PullStatus() types.PullStatus {
|
||||
return manager.pull.Get()
|
||||
}
|
||||
@@ -245,6 +245,30 @@ type RoomMember struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type PullStart struct {
|
||||
NekoImage string `json:"neko_image"`
|
||||
RegistryUser string `json:"registry_user"`
|
||||
RegistryPass string `json:"registry_pass"`
|
||||
}
|
||||
|
||||
type PullLayer struct {
|
||||
Status string `json:"status"`
|
||||
ProgressDetail *struct {
|
||||
Current int `json:"current"`
|
||||
Total int `json:"total"`
|
||||
} `json:"progressDetail"`
|
||||
Progress string `json:"progress"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type PullStatus struct {
|
||||
Active bool `json:"active"`
|
||||
Started *time.Time `json:"started"`
|
||||
Layers map[string]PullLayer `json:"layers"`
|
||||
Status []string `json:"status"`
|
||||
Finished *time.Time `json:"finished"`
|
||||
}
|
||||
|
||||
type RoomManager interface {
|
||||
Config() RoomsConfig
|
||||
List() ([]RoomEntry, error)
|
||||
@@ -259,4 +283,8 @@ type RoomManager interface {
|
||||
Start(id string) error
|
||||
Stop(id string) error
|
||||
Restart(id string) error
|
||||
|
||||
PullStart(request PullStart) error
|
||||
PullStop() error
|
||||
PullStatus() PullStatus
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user