pipelines/frontend/server/handlers/tensorboard.ts

176 lines
5.5 KiB
TypeScript

// 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,
};
};