// Copyright 2019-2021 The Kubeflow Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Handler } from 'express'; import * as k8sHelper from '../k8s-helper'; import { ViewerTensorboardConfig } from '../configs'; import { AuthorizeRequestResources, AuthorizeRequestVerb } from '../src/generated/apis/auth'; import { parseError } from '../utils'; import { AuthorizeFn } from '../helpers/auth'; export const getTensorboardHandlers = ( tensorboardConfig: ViewerTensorboardConfig, authorizeFn: AuthorizeFn, ): { get: Handler; create: Handler; delete: Handler } => { /** * A handler which retrieve the endpoint for a tensorboard instance. The * handler expects a query string `logdir`. */ const get: Handler = async (req, res) => { const { logdir, namespace } = req.query; if (!logdir) { res.status(400).send('logdir argument is required'); return; } if (!namespace) { res.status(400).send('namespace argument is required'); return; } try { const authError = await authorizeFn( { verb: AuthorizeRequestVerb.GET, resources: AuthorizeRequestResources.VIEWERS, namespace, }, req, ); if (authError) { res.status(401).send(authError.message); return; } res.send(await k8sHelper.getTensorboardInstance(logdir, namespace)); } catch (err) { const details = await parseError(err); console.error(`Failed to list Tensorboard pods: ${details.message}`, details.additionalInfo); res.status(500).send(`Failed to list Tensorboard pods: ${details.message}`); } }; /** * A handler which will create a tensorboard viewer CRD, waits for the * tensorboard instance to be ready, and return the endpoint to the instance. * The handler expects the following query strings in the request: * - `logdir` * - `tfversion`, optional. TODO: consider deprecate * - `image`, optional * - `podtemplatespec`, optional * * image or tfversion should be specified. */ const create: Handler = async (req, res) => { const { logdir, namespace, tfversion, image, podtemplatespec: podTemplateSpecRaw } = req.query; if (!logdir) { res.status(400).send('logdir argument is required'); return; } if (!namespace) { res.status(400).send('namespace argument is required'); return; } if (!tfversion && !image) { res.status(400).send('missing required argument: tfversion (tensorflow version) or image'); return; } if (tfversion && image) { res.status(400).send('tfversion and image cannot be specified at the same time'); return; } let podTemplateSpec: any | undefined; if (podTemplateSpecRaw) { try { podTemplateSpec = JSON.parse(podTemplateSpecRaw); } catch (err) { res.status(400).send(`podtemplatespec is not valid JSON: ${err}`); return; } } try { const authError = await authorizeFn( { verb: AuthorizeRequestVerb.CREATE, resources: AuthorizeRequestResources.VIEWERS, namespace, }, req, ); if (authError) { res.status(401).send(authError.message); return; } await k8sHelper.newTensorboardInstance( logdir, namespace, image || tensorboardConfig.tfImageName, tfversion, podTemplateSpec || tensorboardConfig.podTemplateSpec, ); const tensorboardAddress = await k8sHelper.waitForTensorboardInstance( logdir, namespace, 60 * 1000, ); res.send(tensorboardAddress); } catch (err) { const details = await parseError(err); console.error(`Failed to start Tensorboard app: ${details.message}`, details.additionalInfo); res.status(500).send(`Failed to start Tensorboard app: ${details.message}`); } }; /** * A handler that deletes a tensorboard viewer. The handler expects query string * `logdir` in the request. */ const deleteHandler: Handler = async (req, res) => { const { logdir, namespace } = req.query; if (!logdir) { res.status(400).send('logdir argument is required'); return; } if (!namespace) { res.status(400).send('namespace argument is required'); return; } try { const authError = await authorizeFn( { verb: AuthorizeRequestVerb.DELETE, resources: AuthorizeRequestResources.VIEWERS, namespace, }, req, ); if (authError) { res.status(401).send(authError.message); return; } await k8sHelper.deleteTensorboardInstance(logdir, namespace); res.send('Tensorboard deleted.'); } catch (err) { const details = await parseError(err); console.error(`Failed to delete Tensorboard app: ${details.message}`, details.additionalInfo); res.status(500).send(`Failed to delete Tensorboard app: ${details.message}`); } }; return { get, create, delete: deleteHandler, }; };