Skip to content

Commit 383160b

Browse files
committed
WIP
1 parent 738ad91 commit 383160b

13 files changed

+465
-742
lines changed

frontend/server/server.test.ts frontend/server/app.test.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,48 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14+
import * as os from 'os';
1415
import * as fs from 'fs';
1516
import * as path from 'path';
16-
import * as tmp from 'tmp';
1717
import * as requests from 'supertest';
1818

19-
jest.mock('fs');
19+
import { UIServer } from './app';
20+
import { loadConfigs } from './configs';
2021

2122
describe('server', () => {
22-
const mockedReadFile: jest.Mock = fs.readFile as any;
23-
let tmpdir: tmp.DirResult;
23+
const indexHtmlPath = path.resolve(os.tmpdir(), 'index.html');
24+
const argv = ['node', 'dist/server.js', os.tmpdir(), '3000'];
25+
const indexHtmlContent = `
26+
<html>
27+
<head>
28+
<script>
29+
window.KFP_FLAGS.DEPLOYMENT=null
30+
</script>
31+
<script id="kubeflow-client-placeholder"></script>
32+
</head>
33+
</html>`;
2434

2535
beforeAll(() => {
26-
import app from './server';
27-
})
36+
fs.writeFileSync(indexHtmlPath, indexHtmlContent);
37+
});
2838

2939
beforeEach(() => {
30-
tmpdir = tmp.dirSync({unsafeCleanup: true});
3140
jest.resetModules();
32-
process.argv = ['node', 'dist/server.js', tmpdir.name, '3000'];
33-
fs.writeFileSync(path.resolve(tmpdir.name, 'index.html'), '<html></html>');
3441
});
3542

36-
afterEach(() => {
37-
tmpdir.removeCallback();
43+
afterAll(() => {
44+
fs.unlinkSync(indexHtmlPath);
3845
});
3946

4047
it('s', done => {
48+
const configs = loadConfigs(argv, {});
49+
expect(configs.server.port).toBe('3000');
50+
expect(configs.server.staticDir).toBe(os.tmpdir());
51+
52+
const app = new UIServer(configs);
53+
4154
requests(app)
4255
.get('/')
43-
.expect(200, '<html></html>', done);
56+
.expect(200, indexHtmlContent, done);
4457
});
4558
});

frontend/server/app.ts

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
import * as path from 'path';
15+
import * as express from 'express';
16+
import { Application, static as StaticHandler } from 'express';
17+
import * as proxy from 'http-proxy-middleware';
18+
19+
import { IConfigs } from './configs';
20+
import { getAddress } from './utils';
21+
import { getBuildMetadata, getHealthzEndpoint, getHealthzHandler } from './handlers/healthz';
22+
import { getArtifactsHandler } from './handlers/artifacts';
23+
import { getCreateTensorboardHandler, getGetTensorboardHandler } from './handlers/tensorboard';
24+
import { getPodLogsHandler } from './handlers/pod-logs';
25+
import { clusterNameHandler, projectIdHandler } from './handlers/core';
26+
import { getAllowCustomVisualizationsHandler } from './handlers/vis';
27+
import { getIndexHTMLHandler } from './handlers/index-html';
28+
29+
import proxyMiddleware from './proxy-middleware';
30+
31+
function getRegisterHandler(app: Application, basePath: string) {
32+
return (
33+
func: (name: string, handler: express.Handler) => express.Application,
34+
route: string,
35+
handler: express.Handler,
36+
) => {
37+
func.call(app, route, handler);
38+
return func.call(app, `${basePath}${route}`, handler);
39+
};
40+
}
41+
42+
export class UIServer {
43+
app: Application;
44+
constructor(public readonly options: IConfigs) {
45+
this.app = createUIServer(options);
46+
}
47+
48+
start() {
49+
this.app.listen(this.options.server.port, () => {
50+
console.log('Server listening at http://localhost:' + this.options.server.port);
51+
});
52+
return this;
53+
}
54+
}
55+
56+
function createUIServer(options: IConfigs) {
57+
const currDir = path.resolve(__dirname);
58+
const basePath = options.server.basePath;
59+
const apiVersion = options.server.apiVersion;
60+
const apiServerAddress = getAddress(options.pipeline);
61+
const envoyServiceAddress = getAddress(options.metadata.envoyService);
62+
63+
const app: Application = express();
64+
const registerHandler = getRegisterHandler(app, basePath);
65+
66+
app.use(function(req, _, next) {
67+
console.info(req.method + ' ' + req.originalUrl);
68+
next();
69+
});
70+
71+
registerHandler(
72+
app.get,
73+
`/${apiVersion}/healthz`,
74+
getHealthzHandler({
75+
healthzEndpoint: getHealthzEndpoint(apiServerAddress, options.server.apiVersion),
76+
healthzStats: getBuildMetadata(currDir),
77+
}),
78+
);
79+
80+
registerHandler(app.get, '/artifacts/get', getArtifactsHandler(options.artifacts));
81+
82+
registerHandler(app.get, '/apps/tensorboard', getGetTensorboardHandler());
83+
registerHandler(
84+
app.post,
85+
'/apps/tensorboard',
86+
getCreateTensorboardHandler(options.viewer.tensorboard.podTemplateSpec),
87+
);
88+
89+
registerHandler(app.get, '/k8s/pod/logs', getPodLogsHandler(options.argo, options.artifacts));
90+
91+
registerHandler(app.get, '/system/cluster-name', clusterNameHandler);
92+
registerHandler(app.get, '/system/project-id', projectIdHandler);
93+
94+
registerHandler(
95+
app.get,
96+
'/visualizations/allowed',
97+
getAllowCustomVisualizationsHandler(options.visualizations.allowCustomVisualizations),
98+
);
99+
100+
// Proxy metadata requests to the Envoy instance which will handle routing to the metadata gRPC server
101+
app.all(
102+
'/ml_metadata.*',
103+
proxy({
104+
changeOrigin: true,
105+
onProxyReq: proxyReq => {
106+
console.log('Metadata proxied request: ', (proxyReq as any).path);
107+
},
108+
target: envoyServiceAddress,
109+
}),
110+
);
111+
112+
// Order matters here, since both handlers can match any proxied request with a referer,
113+
// and we prioritize the basepath-friendly handler
114+
proxyMiddleware(app, `${basePath}/${apiVersion}`);
115+
proxyMiddleware(app, `/${apiVersion}`);
116+
117+
app.all(
118+
`/${apiVersion}/*`,
119+
proxy({
120+
changeOrigin: true,
121+
onProxyReq: proxyReq => {
122+
console.log('Proxied request: ', (proxyReq as any).path);
123+
},
124+
target: apiServerAddress,
125+
}),
126+
);
127+
128+
app.all(
129+
`${basePath}/${apiVersion}/*`,
130+
proxy({
131+
changeOrigin: true,
132+
onProxyReq: proxyReq => {
133+
console.log('Proxied request: ', (proxyReq as any).path);
134+
},
135+
pathRewrite: path =>
136+
path.startsWith(basePath) ? path.substr(basePath.length, path.length) : path,
137+
target: apiServerAddress,
138+
}),
139+
);
140+
141+
// These pathes can be matched by static handler. Putting them before it to
142+
// override behavior for index html.
143+
const indexHtmlHandler = getIndexHTMLHandler(options.server);
144+
registerHandler(app.get, '/', indexHtmlHandler);
145+
registerHandler(app.get, '/index.html', indexHtmlHandler);
146+
147+
app.use(basePath, StaticHandler(options.server.staticDir));
148+
app.use(StaticHandler(options.server.staticDir));
149+
150+
app.get('*', indexHtmlHandler);
151+
152+
return app;
153+
}

0 commit comments

Comments
 (0)