forked from kubeflow/pipelines
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(frontend): support tensorboard viewer and other visualize Result…
…s using volume mount path. Part of kubeflow#4208 (kubeflow#4236) * support local file storage type for local volume mount path, refer: kubeflow#4208 * add todo comment to support directory and filePath include wildcards '*', detail refer: kubeflow#4208 * revert old code indent * run 'npm run format' to format code * support tensorboard viewer and other visualize Results using volume mount path, modify 'file' schema to 'volume': 1. source schema: volume://volume-name/relative/path/from/volume/xxx.csv 2. for tensorboard(also support Series1:volume://volume-name/path_to_model_dir_1,Series2:volume://volume-name/path_to_model_dir_2): * check volume-name was specified in podTemplateSpec( which was inject by VIEWER_TENSORBOARD_POD_TEMPLATE_SPEC_PATH env) * check /relative/path/from/volume/xxx file path was prefix-mounted in podTemplateSpec 3. for others: * check volume-name was specified at ml-pipeline-ui pod * check /relative/path/from/volume/xxx.csv file path exist * fix test and add more tests * change error message not found to not exist. * fix tensorboard create test * combining volume mount path and key as artifacts path * extra complex code to a function and add more test * use ml-pipeline-ui container name to find server container instead of use containers[0] * fix review suggestion: kubeflow#4236 * format code * extract how to find file path on a pod volume to a common function, and optimize error message * fix k8s-helper.test error * add more documentation and fix mistake: volumeMountPath to filePathInVolume * fix test error * Update k8s-helper.test.ts * format error message Co-authored-by: Yuan (Bob) Gong <gongyuan94@gmail.com>
- Loading branch information
Showing
9 changed files
with
1,183 additions
and
12 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright 2019-2020 Google LLC | ||
// | ||
// 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 * as fs from 'fs'; | ||
import { V1Pod } from '@kubernetes/client-node'; | ||
import { getPod } from '../k8s-helper'; | ||
|
||
const namespaceFilePath = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'; | ||
let serverNamespace: string | undefined; | ||
let hostPod: V1Pod | undefined; | ||
|
||
// The file path contains pod namespace when in Kubernetes cluster. | ||
if (fs.existsSync(namespaceFilePath)) { | ||
serverNamespace = fs.readFileSync(namespaceFilePath, 'utf-8'); | ||
} | ||
|
||
// get ml-pipeline-ui host pod | ||
export async function getHostPod(): Promise<[V1Pod | undefined, undefined] | [undefined, string]> { | ||
// use cached hostPod | ||
if (hostPod) { | ||
return [hostPod, undefined]; | ||
} | ||
|
||
if (!serverNamespace) { | ||
return [undefined, "server namespace can't be obtained"]; | ||
} | ||
|
||
// get ml-pipeline-ui server pod name | ||
const { HOSTNAME: POD_NAME } = process.env; | ||
if (!POD_NAME) { | ||
return [undefined, "server pod name can't be obtained"]; | ||
} | ||
|
||
const [pod, err] = await getPod(POD_NAME, serverNamespace); | ||
|
||
if (err) { | ||
const { message, additionalInfo } = err; | ||
console.error(message, additionalInfo); | ||
return [undefined, `Failed to get host pod: ${message}`]; | ||
} | ||
|
||
hostPod = pod; | ||
return [hostPod, undefined]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// 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 { TEST_ONLY as K8S_TEST_EXPORT } from './k8s-helper'; | ||
|
||
describe('k8s-helper', () => { | ||
describe('parseTensorboardLogDir', () => { | ||
const podTemplateSpec = { | ||
spec: { | ||
containers: [ | ||
{ | ||
volumeMounts: [ | ||
{ | ||
name: 'output', | ||
mountPath: '/data', | ||
}, | ||
{ | ||
name: 'artifact', | ||
subPath: 'pipeline1', | ||
mountPath: '/data1', | ||
}, | ||
{ | ||
name: 'artifact', | ||
subPath: 'pipeline2', | ||
mountPath: '/data2', | ||
}, | ||
], | ||
}, | ||
], | ||
volumes: [ | ||
{ | ||
name: 'output', | ||
hostPath: { | ||
path: '/data/output', | ||
type: 'Directory', | ||
}, | ||
}, | ||
{ | ||
name: 'artifact', | ||
persistentVolumeClaim: { | ||
claimName: 'artifact_pvc', | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
it('handles not volume storage', () => { | ||
const logdir = 'gs://testbucket/test/key/path'; | ||
const url = K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec); | ||
expect(url).toEqual(logdir); | ||
}); | ||
|
||
it('handles not volume storage with Series', () => { | ||
const logdir = | ||
'Series1:gs://testbucket/test/key/path1,Series2:gs://testbucket/test/key/path2'; | ||
const url = K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec); | ||
expect(url).toEqual(logdir); | ||
}); | ||
|
||
it('handles volume storage without subPath', () => { | ||
const logdir = 'volume://output'; | ||
const url = K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec); | ||
expect(url).toEqual('/data'); | ||
}); | ||
|
||
it('handles volume storage without subPath with Series', () => { | ||
const logdir = 'Series1:volume://output/volume/path1,Series2:volume://output/volume/path2'; | ||
const url = K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec); | ||
expect(url).toEqual('Series1:/data/volume/path1,Series2:/data/volume/path2'); | ||
}); | ||
|
||
it('handles volume storage with subPath', () => { | ||
const logdir = 'volume://artifact/pipeline1'; | ||
const url = K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec); | ||
expect(url).toEqual('/data1'); | ||
}); | ||
|
||
it('handles volume storage with subPath with Series', () => { | ||
const logdir = | ||
'Series1:volume://artifact/pipeline1/path1,Series2:volume://artifact/pipeline2/path2'; | ||
const url = K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec); | ||
expect(url).toEqual('Series1:/data1/path1,Series2:/data2/path2'); | ||
}); | ||
|
||
it('handles volume storage without subPath throw volume not configured error', () => { | ||
const logdir = 'volume://other/path'; | ||
expect(() => K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec)).toThrowError( | ||
'Cannot find file "volume://other/path" in pod "unknown": volume "other" not configured', | ||
); | ||
}); | ||
|
||
it('handles volume storage without subPath throw volume not configured error with Series', () => { | ||
const logdir = 'Series1:volume://output/volume/path1,Series2:volume://other/volume/path2'; | ||
expect(() => K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec)).toThrowError( | ||
'Cannot find file "volume://other/volume/path2" in pod "unknown": volume "other" not configured', | ||
); | ||
}); | ||
|
||
it('handles volume storage without subPath throw volume not mounted', () => { | ||
const noMountPodTemplateSpec = { | ||
spec: { | ||
volumes: [ | ||
{ | ||
name: 'artifact', | ||
persistentVolumeClaim: { | ||
claimName: 'artifact_pvc', | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
const logdir = 'volume://artifact/path1'; | ||
expect(() => | ||
K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, noMountPodTemplateSpec), | ||
).toThrowError( | ||
'Cannot find file "volume://artifact/path1" in pod "unknown": container "" not found', | ||
); | ||
}); | ||
|
||
it('handles volume storage without volumeMounts throw volume not mounted', () => { | ||
const noMountPodTemplateSpec = { | ||
spec: { | ||
containers: [ | ||
{ | ||
volumeMounts: [ | ||
{ | ||
name: 'other', | ||
mountPath: '/data', | ||
}, | ||
], | ||
}, | ||
], | ||
volumes: [ | ||
{ | ||
name: 'artifact', | ||
persistentVolumeClaim: { | ||
claimName: 'artifact_pvc', | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
const logdir = 'volume://artifact/path'; | ||
expect(() => K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec)).toThrowError( | ||
'Cannot find file "volume://artifact/path" in pod "unknown": volume "artifact" not mounted', | ||
); | ||
}); | ||
|
||
it('handles volume storage with subPath throw volume mount not found', () => { | ||
const logdir = 'volume://artifact/other'; | ||
expect(() => K8S_TEST_EXPORT.parseTensorboardLogDir(logdir, podTemplateSpec)).toThrowError( | ||
'Cannot find file "volume://artifact/other" in pod "unknown": volume "artifact" not mounted or volume "artifact" with subPath (which is prefix of other) not mounted', | ||
); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.