Skip to content

Commit c4cc924

Browse files
HumairAKsefgsefg
authored andcommitted
correct artifact preview behavior in UI (kubeflow#11059)
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> Signed-off-by: sefgsefg <sefgsefg@gmail.com>
1 parent 43dc23b commit c4cc924

File tree

6 files changed

+109
-16
lines changed

6 files changed

+109
-16
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
@@ -123,6 +123,7 @@ export function loadConfigs(argv: string[], env: ProcessEnv): UIConfigs {
123123
* e.g. a valid header value for default values can be like `accounts.google.com:user@gmail.com`.
124124
*/
125125
KUBEFLOW_USERID_PREFIX = 'accounts.google.com:',
126+
FRONTEND_SERVER_NAMESPACE = 'kubeflow',
126127
} = env;
127128

128129
return {
@@ -190,6 +191,7 @@ export function loadConfigs(argv: string[], env: ProcessEnv): UIConfigs {
190191
: asBool(HIDE_SIDENAV),
191192
port,
192193
staticDir,
194+
serverNamespace: FRONTEND_SERVER_NAMESPACE,
193195
},
194196
viewer: {
195197
tensorboard: {
@@ -266,6 +268,8 @@ export interface ServerConfigs {
266268
apiVersion2Prefix: string;
267269
deployment: Deployments;
268270
hideSideNav: boolean;
271+
// Namespace where the server is deployed
272+
serverNamespace: string;
269273
}
270274
export interface GkeMetadataConfigs {
271275
disabled: boolean;

frontend/server/handlers/artifacts.ts

Lines changed: 8 additions & 4 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,15 +90,18 @@ 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<
99-
ArtifactsQueryStrings
100-
>;
100+
const {
101+
peek = 0,
102+
providerInfo = '',
103+
namespace = options.server.serverNamespace,
104+
} = req.query as Partial<ArtifactsQueryStrings>;
101105
if (!source) {
102106
res.status(500).send('Storage source is missing from artifact request');
103107
return;

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

Lines changed: 85 additions & 10 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,90 @@ 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(
210+
providerInfo,
211+
)}`,
212+
)
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(
225+
1,
226+
'aws-s3-creds',
227+
'AWS_ACCESS_KEY_ID',
228+
`${namespace}`,
229+
);
230+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(
231+
2,
232+
'aws-s3-creds',
233+
'AWS_SECRET_ACCESS_KEY',
234+
`${namespace}`,
235+
);
236+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
237+
done(err);
238+
});
239+
});
240+
241+
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 => {
242+
const mockedGetK8sSecret: jest.Mock = getK8sSecret as any;
243+
mockedGetK8sSecret.mockResolvedValue('somevalue');
244+
const mockedMinioClient: jest.Mock = minio.Client as any;
245+
const namespace = 'notkubeflow';
246+
const configs = loadConfigs(argv, { FRONTEND_SERVER_NAMESPACE: namespace });
247+
app = new UIServer(configs);
248+
const request = requests(app.start());
249+
const providerInfo = {
250+
Params: {
251+
accessKeyKey: 'AWS_ACCESS_KEY_ID',
252+
disableSSL: 'false',
253+
endpoint: 's3.amazonaws.com',
254+
fromEnv: 'false',
255+
region: 'us-east-2',
256+
secretKeyKey: 'AWS_SECRET_ACCESS_KEY',
257+
secretName: 'aws-s3-creds',
258+
},
259+
Provider: 's3',
260+
};
261+
request
262+
.get(
263+
`/artifacts/get?source=s3&bucket=ml-pipeline&key=hello%2Fworld.txt&providerInfo=${JSON.stringify(
207264
providerInfo,
208265
)}`,
209266
)
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);
215-
done(err);
216-
},
217-
);
267+
.expect(200, artifactContent, err => {
268+
expect(mockedMinioClient).toBeCalledWith({
269+
accessKey: 'somevalue',
270+
endPoint: 's3.amazonaws.com',
271+
port: undefined,
272+
region: 'us-east-2',
273+
secretKey: 'somevalue',
274+
useSSL: undefined,
275+
});
276+
expect(mockedMinioClient).toBeCalledTimes(1);
277+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
278+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(
279+
1,
280+
'aws-s3-creds',
281+
'AWS_ACCESS_KEY_ID',
282+
`${namespace}`,
283+
);
284+
expect(mockedGetK8sSecret).toHaveBeenNthCalledWith(
285+
2,
286+
'aws-s3-creds',
287+
'AWS_SECRET_ACCESS_KEY',
288+
`${namespace}`,
289+
);
290+
expect(mockedGetK8sSecret).toBeCalledTimes(2);
291+
done(err);
292+
});
218293
});
219294

220295
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: FRONTEND_SERVER_NAMESPACE
52+
valueFrom:
53+
fieldRef:
54+
fieldPath: metadata.namespace
5155
readinessProbe:
5256
exec:
5357
command:

0 commit comments

Comments
 (0)