Skip to content

Commit 467f30c

Browse files
feat[frontend]: implement artifact-repositories configmap support (#11354)
* feat[frontend]: implement configmap parsing Signed-off-by: droctothorpe <mythicalsunlight@gmail.com> Co-authored-by: quinnovator <jack@jq.codes> * Improve error handling Signed-off-by: droctothorpe <mythicalsunlight@gmail.com> --------- Signed-off-by: droctothorpe <mythicalsunlight@gmail.com> Co-authored-by: quinnovator <jack@jq.codes>
1 parent 533eddc commit 467f30c

File tree

6 files changed

+156
-9
lines changed

6 files changed

+156
-9
lines changed

frontend/server/configs.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ export function loadConfigs(argv: string[], env: ProcessEnv): UIConfigs {
9696
* https://github.com/kubeflow/pipelines/blob/7b7918ebf8c30e6ceec99283ef20dbc02fdf6a42/manifests/kustomize/third-party/argo/base/workflow-controller-configmap-patch.yaml#L28
9797
*/
9898
ARGO_KEYFORMAT = 'artifacts/{{workflow.name}}/{{workflow.creationTimestamp.Y}}/{{workflow.creationTimestamp.m}}/{{workflow.creationTimestamp.d}}/{{pod.name}}',
99+
/** Argo Workflows lets you specify a unique artifact repository for each
100+
* namespace by adding an appropriately formatted configmap to the namespace
101+
* as documented here:
102+
* https://argo-workflows.readthedocs.io/en/latest/artifact-repository-ref/.
103+
* Use this field to enable this lookup. It defaults to false.
104+
*/
105+
ARGO_ARTIFACT_REPOSITORIES_LOOKUP = 'false',
99106
/** Should use server API for log streaming? */
100107
STREAM_LOGS_FROM_SERVER_API = 'false',
101108
/** The main container name of a pod where logs are retrieved */
@@ -132,6 +139,7 @@ export function loadConfigs(argv: string[], env: ProcessEnv): UIConfigs {
132139
archiveBucketName: ARGO_ARCHIVE_BUCKETNAME,
133140
archiveLogs: asBool(ARGO_ARCHIVE_LOGS),
134141
keyFormat: ARGO_KEYFORMAT,
142+
artifactRepositoriesLookup: asBool(ARGO_ARTIFACT_REPOSITORIES_LOOKUP),
135143
},
136144
pod: {
137145
logContainerName: POD_LOG_CONTAINER_NAME,
@@ -259,6 +267,7 @@ export interface ArgoConfigs {
259267
archiveArtifactory: string;
260268
archiveBucketName: string;
261269
keyFormat: string;
270+
artifactRepositoriesLookup: boolean;
262271
}
263272
export interface ServerConfigs {
264273
basePath: string;

frontend/server/handlers/pod-logs.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,21 @@ export function getPodLogsHandler(
3939
},
4040
podLogContainerName: string,
4141
): Handler {
42-
const { archiveLogs, archiveArtifactory, archiveBucketName, keyFormat } = argoOptions;
42+
const {
43+
archiveLogs,
44+
archiveArtifactory,
45+
archiveBucketName,
46+
keyFormat,
47+
artifactRepositoriesLookup,
48+
} = argoOptions;
4349

4450
// get pod log from the provided bucket and keyFormat.
4551
const getPodLogsStreamFromArchive = toGetPodLogsStream(
4652
createPodLogsMinioRequestConfig(
4753
archiveArtifactory === 'minio' ? artifactsOptions.minio : artifactsOptions.aws,
4854
archiveBucketName,
4955
keyFormat,
56+
artifactRepositoriesLookup,
5057
),
5158
);
5259

frontend/server/k8s-helper.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
V1DeleteOptions,
2020
V1Pod,
2121
V1EventList,
22+
V1ConfigMap,
2223
} from '@kubernetes/client-node';
2324
import * as crypto from 'crypto-js';
2425
import * as fs from 'fs';
@@ -277,6 +278,25 @@ export async function getPod(
277278
}
278279
}
279280

281+
/**
282+
* Retrieves a configmap.
283+
* @param configMapName name of the configmap
284+
* @param configMapNamespace namespace of the configmap
285+
*/
286+
export async function getConfigMap(
287+
configMapName: string,
288+
configMapNamespace: string,
289+
): Promise<[V1ConfigMap, undefined] | [undefined, K8sError]> {
290+
try {
291+
const { body } = await k8sV1Client.readNamespacedConfigMap(configMapName, configMapNamespace);
292+
return [body, undefined];
293+
} catch (error) {
294+
const { message, additionalInfo } = await parseError(error);
295+
const userMessage = `Could not get configMap ${configMapName} in namespace ${configMapNamespace}: ${message}`;
296+
return [undefined, { message: userMessage, additionalInfo }];
297+
}
298+
}
299+
280300
// Golang style result type including an error.
281301
export type Result<T, E = K8sError> = [T, undefined] | [undefined, E];
282302
export async function listPodEvents(

frontend/server/workflow-helper.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import {
1919
getPodLogsStreamFromK8s,
2020
getPodLogsStreamFromWorkflow,
2121
toGetPodLogsStream,
22+
getKeyFormatFromArtifactRepositories,
2223
} from './workflow-helper';
23-
import { getK8sSecret, getArgoWorkflow, getPodLogs } from './k8s-helper';
24+
import { getK8sSecret, getArgoWorkflow, getPodLogs, getConfigMap } from './k8s-helper';
25+
import { V1ConfigMap, V1ObjectMeta } from '@kubernetes/client-node';
2426

2527
jest.mock('minio');
2628
jest.mock('./k8s-helper');
@@ -118,13 +120,48 @@ describe('workflow-helper', () => {
118120
});
119121
});
120122

123+
describe('getKeyFormatFromArtifactRepositories', () => {
124+
it('returns a keyFormat string from the artifact-repositories configmap.', async () => {
125+
const artifactRepositories = {
126+
'artifact-repositories':
127+
'archiveLogs: true\n' +
128+
's3:\n' +
129+
' accessKeySecret:\n' +
130+
' key: accesskey\n' +
131+
' name: mlpipeline-minio-artifact\n' +
132+
' bucket: mlpipeline\n' +
133+
' endpoint: minio-service.kubeflow:9000\n' +
134+
' insecure: true\n' +
135+
' keyFormat: foo\n' +
136+
' secretKeySecret:\n' +
137+
' key: secretkey\n' +
138+
' name: mlpipeline-minio-artifact',
139+
};
140+
141+
const mockedConfigMap: V1ConfigMap = {
142+
apiVersion: 'v1',
143+
kind: 'ConfigMap',
144+
metadata: new V1ObjectMeta(),
145+
data: artifactRepositories,
146+
binaryData: {},
147+
};
148+
149+
const mockedGetConfigMap: jest.Mock = getConfigMap as any;
150+
mockedGetConfigMap.mockResolvedValueOnce([mockedConfigMap, undefined]);
151+
const res = await getKeyFormatFromArtifactRepositories('');
152+
expect(mockedGetConfigMap).toBeCalledTimes(1);
153+
expect(res).toEqual('foo');
154+
});
155+
});
156+
121157
describe('createPodLogsMinioRequestConfig', () => {
122158
it('returns a MinioRequestConfig factory with the provided minioClientOptions, bucket, and prefix.', async () => {
123159
const mockedClient: jest.Mock = MinioClient as any;
124160
const requestFunc = await createPodLogsMinioRequestConfig(
125161
minioConfig,
126162
'bucket',
127163
'artifacts/{{workflow.name}}/{{workflow.creationTimestamp.Y}}/{{workflow.creationTimestamp.m}}/{{workflow.creationTimestamp.d}}/{{pod.name}}',
164+
true,
128165
);
129166
const request = await requestFunc(
130167
'workflow-name-system-container-impl-foo',

frontend/server/workflow-helper.ts

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
// limitations under the License.
1414
import { PassThrough, Stream } from 'stream';
1515
import { ClientOptions as MinioClientOptions } from 'minio';
16-
import { getK8sSecret, getArgoWorkflow, getPodLogs } from './k8s-helper';
16+
import { getK8sSecret, getArgoWorkflow, getPodLogs, getConfigMap } from './k8s-helper';
1717
import { createMinioClient, MinioRequestConfig, getObjectStream } from './minio-helper';
18+
import * as JsYaml from 'js-yaml';
1819

1920
export interface PartialArgoWorkflow {
2021
status: {
@@ -142,18 +143,76 @@ export function toGetPodLogsStream(
142143
};
143144
}
144145

146+
/** PartialArtifactRepositoriesValue is used to deserialize the contents of the
147+
* artifact-repositories configmap.
148+
*/
149+
interface PartialArtifactRepositoriesValue {
150+
s3?: {
151+
keyFormat: string;
152+
};
153+
gcs?: {
154+
keyFormat: string;
155+
};
156+
oss?: {
157+
keyFormat: string;
158+
};
159+
artifactory?: {
160+
keyFormat: string;
161+
};
162+
}
163+
164+
/**
165+
* getKeyFormatFromArtifactRepositories attempts to retrieve an
166+
* artifact-repositories configmap from a specified namespace. It then parses
167+
* the configmap and returns a keyFormat value in its data field.
168+
* @param namespace namespace of the configmap
169+
*/
170+
export async function getKeyFormatFromArtifactRepositories(
171+
namespace: string,
172+
): Promise<string | undefined> {
173+
try {
174+
const [configMap, k8sError] = await getConfigMap('artifact-repositories', namespace);
175+
if (configMap === undefined) {
176+
throw k8sError;
177+
}
178+
const artifactRepositories = configMap?.data['artifact-repositories'];
179+
const artifactRepositoriesValue = JsYaml.safeLoad(
180+
artifactRepositories,
181+
) as PartialArtifactRepositoriesValue;
182+
if ('s3' in artifactRepositoriesValue) {
183+
return artifactRepositoriesValue.s3?.keyFormat;
184+
} else if ('gcs' in artifactRepositoriesValue) {
185+
return artifactRepositoriesValue.gcs?.keyFormat;
186+
} else if ('oss' in artifactRepositoriesValue) {
187+
return artifactRepositoriesValue.oss?.keyFormat;
188+
} else if ('artifactory' in artifactRepositoriesValue) {
189+
return artifactRepositoriesValue.artifactory?.keyFormat;
190+
} else {
191+
throw new Error(
192+
'artifact-repositories configmap missing one of [s3|gcs|oss|artifactory] fields.',
193+
);
194+
}
195+
} catch (error) {
196+
console.log(error);
197+
return undefined;
198+
}
199+
}
200+
145201
/**
146-
* Returns a MinioRequestConfig with the provided minio options (a MinioRequestConfig
147-
* object contains the artifact bucket and keys, with the corresponding minio
148-
* client).
202+
* Returns a MinioRequestConfig with the provided minio options (a
203+
* MinioRequestConfig object contains the artifact bucket and keys, with the
204+
* corresponding minio client).
149205
* @param minioOptions Minio options to create a minio client.
150206
* @param bucket bucket containing the pod logs artifacts.
151-
* @param keyFormat the keyFormat for pod logs artifacts stored in the bucket.
207+
* @param keyFormatDefault the default keyFormat for pod logs artifacts stored
208+
* in the bucket. This is overriden if there's an "artifact-repositories"
209+
* configmap in the target namespace with a keyFormat field.
152210
*/
153211
export function createPodLogsMinioRequestConfig(
154212
minioOptions: MinioClientOptions,
155213
bucket: string,
156-
keyFormat: string,
214+
keyFormatDefault: string,
215+
artifactRepositoriesLookup: boolean,
157216
) {
158217
return async (
159218
podName: string,
@@ -164,7 +223,21 @@ export function createPodLogsMinioRequestConfig(
164223
const client = await createMinioClient(minioOptions, 's3');
165224
const createdAtArray = createdAt.split('-');
166225

167-
let key: string = keyFormat
226+
// If artifactRepositoriesLookup is enabled, try to extract they keyformat
227+
// from the configmap. Otherwise, just used the default keyFormat specified
228+
// in configs.ts.
229+
let keyFormatFromConfigMap = undefined;
230+
if (artifactRepositoriesLookup) {
231+
keyFormatFromConfigMap = await getKeyFormatFromArtifactRepositories(namespace);
232+
}
233+
let key: string;
234+
if (keyFormatFromConfigMap !== undefined) {
235+
key = keyFormatFromConfigMap;
236+
} else {
237+
key = keyFormatDefault;
238+
}
239+
240+
key = key
168241
.replace(/\s+/g, '') // Remove all whitespace.
169242
.replace('{{workflow.name}}', podName.replace(/-system-container-impl-.*/, ''))
170243
.replace('{{workflow.creationTimestamp.Y}}', createdAtArray[0])

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ rules:
2222
- ""
2323
resources:
2424
- secrets
25+
- configmaps
2526
verbs:
2627
- get
2728
- list

0 commit comments

Comments
 (0)