Skip to content

Commit f1822b2

Browse files
committed
correct artifact preview behavior in UI
This change allows KFP UI to fallback to UI host namespace when no namespaces are provided when referencing the artifact object store provider secret, in default kubeflow deployments this namespace is simply "kubeflow", however the user can customize this behavior by providing the environment variable "SERVER_NAMESPACE" to the KFP UI deployment. Further more, this change addresses a bug that caused URL parse to fail when parsing endpoints without a protocol, this will support such endpoint types as <ip>:<port> for object store endpoints, as is the case in the default kfp deployment manifests. Signed-off-by: Humair Khan <HumairAK@users.noreply.github.com>
1 parent eea7d48 commit f1822b2

File tree

6 files changed

+86
-13
lines changed

6 files changed

+86
-13
lines changed

frontend/server/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ function createUIServer(options: UIConfigs) {
143143
artifactsConfigs: options.artifacts,
144144
useParameter: false,
145145
tryExtract: true,
146+
options: options,
146147
}),
147148
);
148149
// /artifacts/ endpoint downloads the artifact as is, it does not try to unzip or untar.
@@ -158,6 +159,7 @@ function createUIServer(options: UIConfigs) {
158159
artifactsConfigs: options.artifacts,
159160
useParameter: true,
160161
tryExtract: false,
162+
options: options,
161163
}),
162164
);
163165

frontend/server/configs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export function loadConfigs(argv: string[], env: ProcessEnv): UIConfigs {
120120
* e.g. a valid header value for default values can be like `accounts.google.com:user@gmail.com`.
121121
*/
122122
KUBEFLOW_USERID_PREFIX = 'accounts.google.com:',
123+
SERVER_NAMESPACE = 'kubeflow',
123124
} = env;
124125

125126
return {
@@ -187,6 +188,7 @@ export function loadConfigs(argv: string[], env: ProcessEnv): UIConfigs {
187188
: asBool(HIDE_SIDENAV),
188189
port,
189190
staticDir,
191+
namespace: SERVER_NAMESPACE,
190192
},
191193
viewer: {
192194
tensorboard: {
@@ -263,6 +265,8 @@ export interface ServerConfigs {
263265
apiVersion2Prefix: string;
264266
deployment: Deployments;
265267
hideSideNav: boolean;
268+
// Namespace where the server is deployed
269+
namespace: string;
266270
}
267271
export interface GkeMetadataConfigs {
268272
disabled: boolean;

frontend/server/handlers/artifacts.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414
import fetch from 'node-fetch';
15-
import { AWSConfigs, HttpConfigs, MinioConfigs, ProcessEnv } from '../configs';
15+
import {AWSConfigs, HttpConfigs, MinioConfigs, ProcessEnv, UIConfigs} from '../configs';
1616
import { Client as MinioClient } from 'minio';
1717
import { PreviewStream, findFileOnPodVolume, parseJSONString } from '../utils';
1818
import { createMinioClient, getObjectStream } from '../minio-helper';
@@ -80,6 +80,7 @@ export function getArtifactsHandler({
8080
artifactsConfigs,
8181
useParameter,
8282
tryExtract,
83+
options,
8384
}: {
8485
artifactsConfigs: {
8586
aws: AWSConfigs;
@@ -89,13 +90,14 @@ export function getArtifactsHandler({
8990
};
9091
tryExtract: boolean;
9192
useParameter: boolean;
93+
options: UIConfigs,
9294
}): Handler {
9395
const { aws, http, minio, allowedDomain } = artifactsConfigs;
9496
return async (req, res) => {
9597
const source = useParameter ? req.params.source : req.query.source;
9698
const bucket = useParameter ? req.params.bucket : req.query.bucket;
9799
const key = useParameter ? req.params[0] : req.query.key;
98-
const { peek = 0, providerInfo = '', namespace = '' } = req.query as Partial<
100+
const { peek = 0, providerInfo = '', namespace = options.server.namespace } = req.query as Partial<
99101
ArtifactsQueryStrings
100102
>;
101103
if (!source) {

frontend/server/integration-tests/artifact-get.test.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,11 @@ describe('/artifacts', () => {
184184
});
185185
});
186186

187-
it('responds error when source is s3, and creds are sourced from Provider Configs, but no namespace is provided', done => {
187+
it('responds with artifact if source is AWS S3, and creds are sourced from Provider Configs, and uses default kubeflow namespace when no namespace is provided', done => {
188188
const mockedGetK8sSecret: jest.Mock = getK8sSecret as any;
189+
mockedGetK8sSecret.mockResolvedValue('somevalue');
190+
const mockedMinioClient: jest.Mock = minio.Client as any;
191+
const namespace = 'kubeflow';
189192
const configs = loadConfigs(argv, {});
190193
app = new UIServer(configs);
191194
const request = requests(app.start());
@@ -203,18 +206,72 @@ describe('/artifacts', () => {
203206
};
204207
request
205208
.get(
206-
`/artifacts/get?source=s3&bucket=ml-pipeline&key=hello%2Fworld.txt$&providerInfo=${JSON.stringify(
209+
`/artifacts/get?source=s3&bucket=ml-pipeline&key=hello%2Fworld.txt&providerInfo=${JSON.stringify(
207210
providerInfo,
208211
)}`,
209212
)
210-
.expect(
211-
500,
212-
'Failed to initialize Minio Client for S3 Provider: Error: Artifact Store provider given, but no namespace provided.',
213-
err => {
214-
expect(mockedGetK8sSecret).toBeCalledTimes(0);
213+
.expect(200, artifactContent, err => {
214+
expect(mockedMinioClient).toBeCalledWith({
215+
accessKey: 'somevalue',
216+
endPoint: 's3.amazonaws.com',
217+
port: undefined,
218+
region: 'us-east-2',
219+
secretKey: 'somevalue',
220+
useSSL: undefined,
221+
});
222+
expect(mockedMinioClient).toBeCalledTimes(1);
223+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
224+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(1, 'aws-s3-creds', 'AWS_ACCESS_KEY_ID', `${namespace}`);
225+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(2, 'aws-s3-creds', 'AWS_SECRET_ACCESS_KEY', `${namespace}`);
226+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
215227
done(err);
216-
},
217-
);
228+
});
229+
230+
});
231+
232+
233+
it('responds with artifact if source is AWS S3, and creds are sourced from Provider Configs, and uses default namespace when no namespace is provided, as specified in ENV', done => {
234+
const mockedGetK8sSecret: jest.Mock = getK8sSecret as any;
235+
mockedGetK8sSecret.mockResolvedValue('somevalue');
236+
const mockedMinioClient: jest.Mock = minio.Client as any;
237+
const namespace = 'notkubeflow';
238+
const configs = loadConfigs(argv, {SERVER_NAMESPACE: namespace});
239+
app = new UIServer(configs);
240+
const request = requests(app.start());
241+
const providerInfo = {
242+
Params: {
243+
accessKeyKey: 'AWS_ACCESS_KEY_ID',
244+
disableSSL: 'false',
245+
endpoint: 's3.amazonaws.com',
246+
fromEnv: 'false',
247+
region: 'us-east-2',
248+
secretKeyKey: 'AWS_SECRET_ACCESS_KEY',
249+
secretName: 'aws-s3-creds',
250+
},
251+
Provider: 's3',
252+
};
253+
request
254+
.get(
255+
`/artifacts/get?source=s3&bucket=ml-pipeline&key=hello%2Fworld.txt&providerInfo=${JSON.stringify(
256+
providerInfo,
257+
)}`,
258+
)
259+
.expect(200, artifactContent, err => {
260+
expect(mockedMinioClient).toBeCalledWith({
261+
accessKey: 'somevalue',
262+
endPoint: 's3.amazonaws.com',
263+
port: undefined,
264+
region: 'us-east-2',
265+
secretKey: 'somevalue',
266+
useSSL: undefined,
267+
});
268+
expect(mockedMinioClient).toBeCalledTimes(1);
269+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
270+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(1, 'aws-s3-creds', 'AWS_ACCESS_KEY_ID', `${namespace}`);
271+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(2, 'aws-s3-creds', 'AWS_SECRET_ACCESS_KEY', `${namespace}`);
272+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
273+
done(err);
274+
});
218275
});
219276

220277
it('responds with artifact if source is s3-compatible, and creds are sourced from Provider Configs', done => {

frontend/server/minio-helper.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export async function createMinioClient(
7878
}
7979

8080
// If using s3 and sourcing credentials from environment (currently only aws is supported)
81-
if (providerType === 's3' && (!config.accessKey || !config.secretKey)) {
81+
if (providerType === 's3' && !(config.accessKey && config.secretKey)) {
8282
// AWS S3 with credentials from provider chain
8383
if (isAWSS3Endpoint(config.endPoint)) {
8484
try {
@@ -168,7 +168,11 @@ async function parseS3ProviderInfo(
168168
config.useSSL = undefined;
169169
} else {
170170
if (providerInfo.Params.endpoint) {
171-
const parseEndpoint = new URL(providerInfo.Params.endpoint);
171+
const url = providerInfo.Params.endpoint
172+
// this is a bit of a hack to add support for endpoints without a protocol (required by WHATWG URL standard)
173+
// example: <ip>:<port> format. In general should expect most endpoints to provide a protocol as serviced
174+
// by the backend
175+
const parseEndpoint = new URL(url.startsWith("http") ? url : `https://${url}`);
172176
const host = parseEndpoint.hostname;
173177
const port = parseEndpoint.port;
174178
config.endPoint = host;

manifests/kustomize/base/pipeline/ml-pipeline-ui-deployment.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ spec:
4848
key: secretkey
4949
- name: ALLOW_CUSTOM_VISUALIZATIONS
5050
value: "true"
51+
- name: SERVER_NAMESPACE
52+
valueFrom:
53+
fieldRef:
54+
fieldPath: metadata.namespace
5155
readinessProbe:
5256
exec:
5357
command:

0 commit comments

Comments
 (0)