Nuclio as a plugin in CVAT, improved system to check installed plugins (#2192)

* allow to run cvat without nuclio

* fix new line

* fix comments

* Updated core version

* refactoring

* minor refactoring, fixed eslint issues, added documentation to cvat-core, updated ui version, updated changelog

* move plugins to serverViewSet

Co-authored-by: Boris Sekachev <boris.sekachev@yandex.ru>
main
Dmitry Agapov 5 years ago committed by GitHub
parent 6370f980eb
commit f2c84a2653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- UI models (like DEXTR) were redesigned to be more interactive (<https://github.com/opencv/cvat/pull/2054>)
- Used Ubuntu:20.04 as a base image for CVAT Dockerfile (<https://github.com/opencv/cvat/pull/2101>)
- Right colors of label tags in label mapping when a user runs automatic detection (<https://github.com/openvinotoolkit/cvat/pull/2162>)
- Nuclio became an optional component of CVAT (<https://github.com/openvinotoolkit/cvat/pull/2192>)
- A key to remove a point from a polyshape [Ctrl => Alt] (<https://github.com/openvinotoolkit/cvat/pull/2204>)
### Deprecated

@ -63,6 +63,7 @@ services:
DJANGO_LOG_SERVER_PORT: 5000
DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:

@ -0,0 +1,7 @@
## Serverless for Computer Vision Annotation Tool (CVAT)
### Run docker container
```bash
# From project root directory
docker-compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
```

@ -0,0 +1,28 @@
version: '2.3'
services:
serverless:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.4.8-amd64
restart: always
networks:
default:
aliases:
- nuclio
volumes:
- /tmp:/tmp
- /var/run/docker.sock:/var/run/docker.sock
environment:
http_proxy:
https_proxy:
no_proxy: 172.28.0.1,${no_proxy}
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: "true"
ports:
- "8070:8070"
cvat:
environment:
CVAT_SERVERLESS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes:
cvat_events:

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.7.1",
"version": "3.8.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.7.1",
"version": "3.8.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {

@ -223,6 +223,11 @@
return tasks;
};
cvat.server.installedApps.implementation = async () => {
const result = await serverProxy.server.installedApps();
return result;
};
return cvat;
}

@ -136,7 +136,6 @@ function build() {
return result;
},
/**
* Method allows to register on a server
* @method register
* @async
@ -272,6 +271,20 @@ function build() {
.apiWrapper(cvat.server.request, url, data);
return result;
},
/**
* Method returns apps that are installed on the server
* @method installedApps
* @async
* @memberof module:API.cvat.server
* @returns {Object} map {installedApp: boolean}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async installedApps() {
const result = await PluginRegistry.apiWrapper(cvat.server.installedApps);
return result;
},
},
/**
* Namespace is used for getting tasks
@ -470,34 +483,35 @@ function build() {
return result;
},
/**
* Install plugin to CVAT
* @method register
* @async
* @memberof module:API.cvat.plugins
* @param {Plugin} [plugin] plugin for registration
* @throws {module:API.cvat.exceptions.PluginError}
*/
* Install plugin to CVAT
* @method register
* @async
* @memberof module:API.cvat.plugins
* @param {Plugin} [plugin] plugin for registration
* @throws {module:API.cvat.exceptions.PluginError}
*/
async register(plugin) {
const result = await PluginRegistry
.apiWrapper(cvat.plugins.register, plugin);
return result;
},
},
/**
* Namespace is used for serverless functions management (mainly related with DL models)
* @namespace lambda
* @memberof module:API.cvat
*/
* Namespace is used for serverless functions management (mainly related with DL models)
* @namespace lambda
* @memberof module:API.cvat
*/
lambda: {
/**
* Method returns list of available serverless models
* @method list
* @async
* @memberof module:API.cvat.lambda
* @returns {module:API.cvat.classes.MLModel[]}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
* Method returns list of available serverless models
* @method list
* @async
* @memberof module:API.cvat.lambda
* @returns {module:API.cvat.classes.MLModel[]}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async list() {
const result = await PluginRegistry
.apiWrapper(cvat.lambda.list);

@ -812,6 +812,18 @@
}
}
async function installedApps() {
const { backendAPI } = config;
try {
const response = await Axios.get(`${backendAPI}/server/plugins`, {
proxy: config.proxy,
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
@ -828,6 +840,7 @@
register,
request: serverRequest,
userAgreements,
installedApps,
}),
writable: false,
},

@ -3,43 +3,35 @@
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { SupportedPlugins } from 'reducers/interfaces';
import PluginChecker from 'utils/plugin-checker';
import { PluginsList } from 'reducers/interfaces';
import getCore from '../cvat-core-wrapper';
const core = getCore();
export enum PluginsActionTypes {
CHECK_PLUGINS = 'CHECK_PLUGINS',
CHECKED_ALL_PLUGINS = 'CHECKED_ALL_PLUGINS',
GET_PLUGINS = 'GET_PLUGINS',
GET_PLUGINS_SUCCESS = 'GET_PLUGINS_SUCCESS',
GET_PLUGINS_FAILED = 'GET_PLUGINS_FAILED',
}
type PluginObjects = Record<SupportedPlugins, boolean>;
const pluginActions = {
checkPlugins: () => createAction(PluginsActionTypes.CHECK_PLUGINS),
checkedAllPlugins: (list: PluginObjects) => (
createAction(PluginsActionTypes.CHECKED_ALL_PLUGINS, {
list,
})
checkPlugins: () => createAction(PluginsActionTypes.GET_PLUGINS),
checkPluginsSuccess: (list: PluginsList) => createAction(
PluginsActionTypes.GET_PLUGINS_SUCCESS, { list },
),
checkPluginsFailed: (error: any) => createAction(
PluginsActionTypes.GET_PLUGINS_FAILED, { error },
),
};
export type PluginActions = ActionUnion<typeof pluginActions>;
export function checkPluginsAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
dispatch(pluginActions.checkPlugins());
const plugins: PluginObjects = {
ANALYTICS: false,
GIT_INTEGRATION: false,
};
const promises: Promise<boolean>[] = [
// check must return true/false with no exceptions
PluginChecker.check(SupportedPlugins.ANALYTICS),
PluginChecker.check(SupportedPlugins.GIT_INTEGRATION),
];
const values = await Promise.all(promises);
[plugins.ANALYTICS, plugins.GIT_INTEGRATION] = values;
dispatch(pluginActions.checkedAllPlugins(plugins));
};
}
export const getPluginsAsync = (): ThunkAction => async (dispatch): Promise<void> => {
dispatch(pluginActions.checkPlugins());
try {
const list: PluginsList = await core.server.installedApps();
dispatch(pluginActions.checkPluginsSuccess(list));
} catch (error) {
dispatch(pluginActions.checkPluginsFailed(error));
}
};

@ -65,6 +65,7 @@ interface CVATAppProps {
authActionsInitialized: boolean;
notifications: NotificationsState;
user: any;
isModelPluginActive: boolean;
}
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
@ -115,6 +116,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
userAgreementsInitialized,
authActionsFetching,
authActionsInitialized,
isModelPluginActive,
} = this.props;
this.showErrors();
@ -150,7 +152,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
loadAbout();
}
if (!modelsInitialized && !modelsFetching) {
if (isModelPluginActive && !modelsInitialized && !modelsFetching) {
initModels();
}
@ -248,11 +250,12 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
switchSettingsDialog,
user,
keyMap,
isModelPluginActive,
} = this.props;
const readyForRender = (userInitialized && (user == null || !user.isVerified))
|| (userInitialized && formatsInitialized
&& pluginsInitialized && usersInitialized && aboutInitialized);
|| (userInitialized && formatsInitialized && pluginsInitialized
&& usersInitialized && aboutInitialized);
const subKeyMap = {
SWITCH_SHORTCUTS: keyMap.SWITCH_SHORTCUTS,
@ -316,7 +319,8 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route exact path='/tasks/:tid/jobs/:jid' component={AnnotationPageContainer} />
<Route exact path='/models' component={ModelsPageContainer} />
{ isModelPluginActive
&& <Route exact path='/models' component={ModelsPageContainer} /> }
<Redirect push to='/tasks' />
</Switch>
</GlobalHotKeys>

@ -22,7 +22,7 @@ import { CVATLogo, AccountIcon } from 'icons';
import ChangePasswordDialog from 'components/change-password-modal/change-password-modal';
import { switchSettingsDialog as switchSettingsDialogAction } from 'actions/settings-actions';
import { logoutAsync, authActions } from 'actions/auth-actions';
import { SupportedPlugins, CombinedState } from 'reducers/interfaces';
import { CombinedState } from 'reducers/interfaces';
import SettingsModal from './settings-modal/settings-modal';
const core = getCore();
@ -53,8 +53,10 @@ interface StateToProps {
changePasswordDialogShown: boolean;
changePasswordFetching: boolean;
logoutFetching: boolean;
installedAnalytics: boolean;
renderChangePasswordItem: boolean;
isAnalyticsPluginActive: boolean;
isModelsPluginActive: boolean;
isGitPluginActive: boolean;
}
interface DispatchToProps {
@ -111,8 +113,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
changePasswordDialogShown,
changePasswordFetching,
logoutFetching,
installedAnalytics: list[SupportedPlugins.ANALYTICS],
renderChangePasswordItem,
isAnalyticsPluginActive: list.ANALYTICS,
isModelsPluginActive: list.MODELS,
isGitPluginActive: list.GIT_INTEGRATION,
};
}
@ -132,7 +136,6 @@ function HeaderContainer(props: Props): JSX.Element {
const {
user,
tool,
installedAnalytics,
logoutFetching,
changePasswordFetching,
settingsDialogShown,
@ -141,6 +144,8 @@ function HeaderContainer(props: Props): JSX.Element {
switchSettingsDialog,
switchChangePasswordDialog,
renderChangePasswordItem,
isAnalyticsPluginActive,
isModelsPluginActive,
} = props;
const {
@ -276,38 +281,40 @@ function HeaderContainer(props: Props): JSX.Element {
>
Tasks
</Button>
<Button
className='cvat-header-button'
type='link'
value='models'
href='/models'
onClick={
(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/models');
{isModelsPluginActive && (
<Button
className='cvat-header-button'
type='link'
value='models'
href='/models'
onClick={
(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/models');
}
}
}
>
Models
</Button>
{ installedAnalytics
&& (
<Button
className='cvat-header-button'
type='link'
href={`${tool.server.host}/analytics/app/kibana`}
onClick={
(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line
window.open(`${tool.server.host}/analytics/app/kibana`, '_blank');
}
>
Models
</Button>
)}
{isAnalyticsPluginActive && (
<Button
className='cvat-header-button'
type='link'
href={`${tool.server.host}/analytics/app/kibana`}
onClick={
(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line
window.open(`${tool.server.host}/analytics/app/kibana`, '_blank');
}
>
Analytics
</Button>
)}
}
>
Analytics
</Button>
)}
</div>
<div className='cvat-right-header'>
<Button

@ -19,22 +19,15 @@ import {
loadAuthActionsAsync,
} from 'actions/auth-actions';
import { getFormatsAsync } from 'actions/formats-actions';
import { checkPluginsAsync } from 'actions/plugins-actions';
import { getPluginsAsync } from 'actions/plugins-actions';
import { getUsersAsync } from 'actions/users-actions';
import { getAboutAsync } from 'actions/about-actions';
import { getModelsAsync } from 'actions/models-actions';
import { getUserAgreementsAsync } from 'actions/useragreements-actions';
import { shortcutsActions } from 'actions/shortcuts-actions';
import { switchSettingsDialog } from 'actions/settings-actions';
import {
resetErrors,
resetMessages,
} from './actions/notification-actions';
import {
CombinedState,
NotificationsState,
} from './reducers/interfaces';
import { resetErrors, resetMessages } from './actions/notification-actions';
import { CombinedState, NotificationsState } from './reducers/interfaces';
createCVATStore(createRootReducer);
const cvatStore = getCVATStore();
@ -61,6 +54,7 @@ interface StateToProps {
notifications: NotificationsState;
user: any;
keyMap: Record<string, ExtendedKeyMapOptions>;
isModelPluginActive: boolean;
}
interface DispatchToProps {
@ -110,6 +104,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
notifications: state.notifications,
user: auth.user,
keyMap: shortcuts.keyMap,
isModelPluginActive: plugins.list.MODELS,
};
}
@ -118,7 +113,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadFormats: (): void => dispatch(getFormatsAsync()),
verifyAuthorized: (): void => dispatch(authorizedAsync()),
loadUserAgreements: (): void => dispatch(getUserAgreementsAsync()),
initPlugins: (): void => dispatch(checkPluginsAsync()),
initPlugins: (): void => dispatch(getPluginsAsync()),
initModels: (): void => dispatch(getModelsAsync()),
loadUsers: (): void => dispatch(getUsersAsync()),
loadAbout: (): void => dispatch(getAboutAsync()),

@ -80,14 +80,17 @@ export interface FormatsState {
export enum SupportedPlugins {
GIT_INTEGRATION = 'GIT_INTEGRATION',
ANALYTICS = 'ANALYTICS',
MODELS = 'MODELS',
}
export type PluginsList = {
[name in SupportedPlugins]: boolean;
};
export interface PluginsState {
fetching: boolean;
initialized: boolean;
list: {
[name in SupportedPlugins]: boolean;
};
list: PluginsList;
}
export interface UsersState {
@ -478,6 +481,14 @@ export interface ShortcutsState {
normalizedKeyMap: Record<string, string>;
}
export interface MetaState {
initialized: boolean;
fetching: boolean;
showTasksButton: boolean;
showAnalyticsButton: boolean;
showModelsButton: boolean;
}
export interface CombinedState {
auth: AuthState;
tasks: TasksState;
@ -492,4 +503,5 @@ export interface CombinedState {
annotation: AnnotationState;
settings: SettingsState;
shortcuts: ShortcutsState;
meta: MetaState;
}

@ -12,6 +12,7 @@ const defaultState: PluginsState = {
list: {
GIT_INTEGRATION: false,
ANALYTICS: false,
MODELS: false,
},
};
@ -20,14 +21,14 @@ export default function (
action: PluginActions,
): PluginsState {
switch (action.type) {
case PluginsActionTypes.CHECK_PLUGINS: {
case PluginsActionTypes.GET_PLUGINS: {
return {
...state,
initialized: false,
fetching: true,
};
}
case PluginsActionTypes.CHECKED_ALL_PLUGINS: {
case PluginsActionTypes.GET_PLUGINS_SUCCESS: {
const { list } = action.payload;
if (!state.list.GIT_INTEGRATION && list.GIT_INTEGRATION) {
@ -41,6 +42,13 @@ export default function (
list,
};
}
case PluginsActionTypes.GET_PLUGINS_FAILED: {
return {
...state,
initialized: true,
fetching: false,
};
}
default:
return state;
}

@ -17,6 +17,7 @@ import settingsReducer from './settings-reducer';
import shortcutsReducer from './shortcuts-reducer';
import userAgreementsReducer from './useragreements-reducer';
export default function createRootReducer(): Reducer {
return combineReducers({
auth: authReducer,

@ -1,29 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import getCore from 'cvat-core-wrapper';
import { SupportedPlugins } from 'reducers/interfaces';
import isReachable from './url-checker';
const core = getCore();
// Easy plugin checker to understand what plugins supports by a server
class PluginChecker {
public static async check(plugin: SupportedPlugins): Promise<boolean> {
const serverHost = core.config.backendAPI.slice(0, -7);
switch (plugin) {
case SupportedPlugins.GIT_INTEGRATION: {
return isReachable(`${serverHost}/git/repository/meta/get`, 'OPTIONS');
}
case SupportedPlugins.ANALYTICS: {
return isReachable(`${serverHost}/analytics/app/kibana`, 'GET');
}
default:
return false;
}
}
}
export default PluginChecker;

@ -398,6 +398,11 @@ class FrameMetaSerializer(serializers.Serializer):
height = serializers.IntegerField()
name = serializers.CharField(max_length=1024)
class PluginsSerializer(serializers.Serializer):
GIT_INTEGRATION = serializers.BooleanField()
ANALYTICS = serializers.BooleanField()
MODELS = serializers.BooleanField()
class DataMetaSerializer(serializers.ModelSerializer):
frames = FrameMetaSerializer(many=True, allow_null=True)
image_quality = serializers.IntegerField(min_value=0, max_value=100)

@ -7,9 +7,11 @@ import os.path as osp
import shutil
import traceback
from datetime import datetime
from distutils.util import strtobool
from tempfile import mkstemp
import django_rq
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import User
from django.db import IntegrityError
@ -40,7 +42,8 @@ from cvat.apps.engine.serializers import (
DataMetaSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectSerializer, RqStatusSerializer,
TaskSerializer, UserSerializer)
TaskSerializer, UserSerializer, PluginsSerializer,
)
from cvat.apps.engine.utils import av_scan_paths
from . import models, task
@ -168,6 +171,23 @@ class ServerViewSet(viewsets.ViewSet):
data = dm.views.get_all_formats()
return Response(DatasetFormatsSerializer(data).data)
@staticmethod
@swagger_auto_schema(method='get', operation_summary='Method provides allowed plugins.',
responses={'200': PluginsSerializer()})
@action(detail=False, methods=['GET'], url_path='plugins', serializer_class=PluginsSerializer)
def plugins(request):
response = {
'GIT_INTEGRATION': apps.is_installed('cvat.apps.git'),
'ANALYTICS': False,
'MODELS': False,
}
if strtobool(os.environ.get("CVAT_ANALYTICS", '0')):
response['ANALYTICS'] = True
if strtobool(os.environ.get("CVAT_SERVERLESS", '0')):
response['MODELS'] = True
return Response(response)
class ProjectFilter(filters.FilterSet):
name = filters.CharFilter(field_name="name", lookup_expr="icontains")
owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")

@ -2,9 +2,9 @@
#
# SPDX-License-Identifier: MIT
from django.urls import path
from django.urls import include, path
from rest_framework import routers
from django.urls import include
from . import views
router = routers.DefaultRouter(trailing_slash=False)
@ -18,7 +18,7 @@ router.register('requests', views.RequestViewSet, basename='request')
# GET /api/v1/lambda/functions - get list of functions
# GET /api/v1/lambda/functions/<int:fid> - get information about the function
# POST /api/v1/labmda/requests - call a function
# POST /api/v1/lambda/requests - call a function
# { "function": "<id>", "mode": "online|offline", "job": "<jid>", "frame": "<n>",
# "points": [...], }
# GET /api/v1/lambda/requests - get list of requests
@ -26,4 +26,4 @@ router.register('requests', views.RequestViewSet, basename='request')
# DEL /api/v1/lambda/requests/<int:rid> - cancel a request (don't delete)
urlpatterns = [
path('api/v1/lambda/', include((router.urls, 'cvat'), namespace='v1'))
]
]

@ -1,4 +1,3 @@
# Copyright (C) 2018-2019 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -19,9 +18,9 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.apps import apps
from django.contrib import admin
from django.urls import path, include
from django.apps import apps
urlpatterns = [
path('admin/', admin.site.urls),

@ -95,25 +95,6 @@ services:
- ./cvat_proxy/conf.d/cvat.conf.template:/etc/nginx/conf.d/cvat.conf.template:ro
command: /bin/sh -c "envsubst '$$CVAT_HOST' < /etc/nginx/conf.d/cvat.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
serverless:
container_name: nuclio
image: quay.io/nuclio/dashboard:1.4.8-amd64
restart: always
networks:
default:
aliases:
- nuclio
volumes:
- /tmp:/tmp
- /var/run/docker.sock:/var/run/docker.sock
environment:
http_proxy:
https_proxy:
no_proxy: 172.28.0.1,${no_proxy}
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: "true"
ports:
- "8070:8070"
networks:
default:
ipam:

Loading…
Cancel
Save