Skip to content

Commit

Permalink
feat: Allow to devfile metadata via ConfigMap
Browse files Browse the repository at this point in the history
Signed-off-by: Anatolii Bazko <abazko@redhat.com>
  • Loading branch information
tolusha committed Sep 4, 2023
1 parent 75d9743 commit 89c22da
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 9 deletions.
8 changes: 8 additions & 0 deletions packages/common/src/dto/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,11 @@ export interface IDevWorkspaceResources {
editorId: string | undefined;
editorContent: string | undefined;
}

export interface IGettingStartedSample {
displayName: string;
description?: string;
icon: { base64data: string; mediatype: string };
url: string;
tags?: Array<string>;
}
3 changes: 3 additions & 0 deletions packages/dashboard-backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { registerWebsocket } from './routes/api/websocket';
import { registerYamlResolverRoute } from './routes/api/yamlResolver';
import { registerFactoryAcceptanceRedirect } from './routes/factoryAcceptanceRedirect';
import { registerWorkspaceRedirect } from './routes/workspaceRedirect';
import { registerGettingStartedSampleRoutes } from './routes/api/gettingStartedSample';

export default async function buildApp(server: FastifyInstance): Promise<void> {
const cheHost = process.env.CHE_HOST as string;
Expand Down Expand Up @@ -110,4 +111,6 @@ export default async function buildApp(server: FastifyInstance): Promise<void> {
registerDevworkspaceResourcesRoute(server);

registerPersonalAccessTokenRoutes(server);

registerGettingStartedSampleRoutes(server);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { LogsApiService } from '../services/logsApi';
import { PodApiService } from '../services/podApi';
import { ServerConfigApiService } from '../services/serverConfigApi';
import { UserProfileApiService } from '../services/userProfileApi';
import { GettingStartedSample } from '../services/gettingStartedSamplesApi';

jest.mock('../services/devWorkspaceApi.ts');

Expand Down Expand Up @@ -49,5 +50,6 @@ describe('DevWorkspace client', () => {
expect(client.podApi).toBeInstanceOf(PodApiService);
expect(client.serverConfigApi).toBeInstanceOf(ServerConfigApiService);
expect(client.userProfileApi).toBeInstanceOf(UserProfileApiService);
expect(client.gettingStartedSampleApi).toBeInstanceOf(GettingStartedSample);
});
});
7 changes: 6 additions & 1 deletion packages/dashboard-backend/src/devworkspaceClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { PersonalAccessTokenService } from './services/personalAccessTokenApi';
import { PodApiService } from './services/podApi';
import { ServerConfigApiService } from './services/serverConfigApi';
import { UserProfileApiService } from './services/userProfileApi';
import { IPodmanApi } from './types/index';
import { IGettingStartedSampleApi, IPodmanApi } from './types/index';
import {
IDevWorkspaceApi,
IDevWorkspaceClient,
Expand All @@ -36,6 +36,7 @@ import {
IServerConfigApi,
IUserProfileApi,
} from './types';
import { GettingStartedSample } from './services/gettingStartedSamplesApi';

export * from './types';

Expand Down Expand Up @@ -89,4 +90,8 @@ export class DevWorkspaceClient implements IDevWorkspaceClient {
get personalAccessTokenApi(): IPersonalAccessTokenApi {
return new PersonalAccessTokenService(this.kubeConfig);
}

get gettingStartedSampleApi(): IGettingStartedSampleApi {
return new GettingStartedSample(this.kubeConfig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2018-2023 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import * as mockClient from '@kubernetes/client-node';
import { CustomObjectsApi } from '@kubernetes/client-node';
import { GettingStartedSample } from '../gettingStartedSamplesApi';
import { api } from '@eclipse-che/common';

describe('Getting Started Samples API Service', () => {
const env = process.env;
let gettingStartedSample: GettingStartedSample;

beforeEach(() => {
jest.resetModules();
process.env = {
CHECLUSTER_CR_NAMESPACE: 'eclipse-che',
};

const { KubeConfig } = mockClient;
const kubeConfig = new KubeConfig();

kubeConfig.makeApiClient = jest.fn().mockImplementation((_api: unknown) => {
return {
listNamespacedConfigMap: () => {
return Promise.resolve({
body: { items: [{ data: { mySample: JSON.stringify(getGettingStartedSample()) } }] },
});
},
} as unknown as CustomObjectsApi;
});

gettingStartedSample = new GettingStartedSample(kubeConfig);
});

afterEach(() => {
process.env = env;
jest.clearAllMocks();
});

test('fetching metadata', async () => {
const res = await gettingStartedSample.list();
expect(res).toEqual([getGettingStartedSample()]);
});
});

function getGettingStartedSample(): api.IGettingStartedSample {
return {
displayName: 'Eclipse Che Dashboard',
description: 'Specifies development environment needed to develop the Eclipse Che Dashboard.',
tags: ['Eclipse Che', 'Dashboard'],
url: 'https://github.com/che-incubator/quarkus-api-example/',
icon: {
base64data: 'base64-encoded-data',
mediatype: 'image/png',
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2023 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { IGettingStartedSampleApi } from '../types';
import * as k8s from '@kubernetes/client-node';
import { api } from '@eclipse-che/common';
import { CoreV1API, prepareCoreV1API } from './helpers/prepareCoreV1API';
import { createError } from './helpers/createError';

const API_ERROR_LABEL = 'CORE_V1_API_ERROR';
const DEVFILE_METADATA_LABEL_SELECTOR =
'app.kubernetes.io/component=getting-started-samples,app.kubernetes.io/part-of=che.eclipse.org';
const DEFAULT_ICON = {
base64data:
'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgaGVpZ2h0PSI1ZW0iIHdpZHRoPSI1ZW0iIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8ZyBmaWxsPSIjNmE2ZTczIj4KICA8cGF0aAogICAgICBkPSJNNDg4LjYgMjUwLjJMMzkyIDIxNFYxMDUuNWMwLTE1LTkuMy0yOC40LTIzLjQtMzMuN2wtMTAwLTM3LjVjLTguMS0zLjEtMTcuMS0zLjEtMjUuMyAwbC0xMDAgMzcuNWMtMTQuMSA1LjMtMjMuNCAxOC43LTIzLjQgMzMuN1YyMTRsLTk2LjYgMzYuMkM5LjMgMjU1LjUgMCAyNjguOSAwIDI4My45VjM5NGMwIDEzLjYgNy43IDI2LjEgMTkuOSAzMi4ybDEwMCA1MGMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAzLjktNTIgMTAzLjkgNTJjMTAuMSA1LjEgMjIuMSA1LjEgMzIuMiAwbDEwMC01MGMxMi4yLTYuMSAxOS45LTE4LjYgMTkuOS0zMi4yVjI4My45YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43ek0zNTggMjE0LjhsLTg1IDMxLjl2LTY4LjJsODUtMzd2NzMuM3pNMTU0IDEwNC4xbDEwMi0zOC4yIDEwMiAzOC4ydi42bC0xMDIgNDEuNC0xMDItNDEuNHYtLjZ6bTg0IDI5MS4xbC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnptMjQwIDExMmwtODUgNDIuNXYtNzkuMWw4NS0zOC44djc1LjR6bTAtMTEybC0xMDIgNDEuNC0xMDItNDEuNHYtLjZsMTAyLTM4LjIgMTAyIDM4LjJ2LjZ6Ij48L3BhdGg+CiAgPC9nPgo8L3N2Zz4K',
mediatype: 'image/svg+xml',
};

export class GettingStartedSample implements IGettingStartedSampleApi {
private readonly coreV1API: CoreV1API;
constructor(kubeConfig: k8s.KubeConfig) {
this.coreV1API = prepareCoreV1API(kubeConfig);
}

private get env(): { NAMESPACE?: string } {
return {
NAMESPACE: process.env.CHECLUSTER_CR_NAMESPACE,
};
}

async list(): Promise<Array<api.IGettingStartedSample>> {
if (!this.env.NAMESPACE) {
console.warn('Mandatory environment variables are not defined: $CHECLUSTER_CR_NAMESPACE');
return [];
}

let response;
try {
response = await this.coreV1API.listNamespacedConfigMap(
this.env.NAMESPACE,
undefined,
undefined,
undefined,
undefined,
DEVFILE_METADATA_LABEL_SELECTOR,
);
} catch (error) {
const additionalMessage = 'Unable to list getting started samples ConfigMap';
throw createError(error, API_ERROR_LABEL, additionalMessage);
}

const samples = [];

for (const cm of response.body.items) {
if (cm.data) {
for (const key in cm.data) {
try {
const sample = JSON.parse(cm.data[key]);
Array.isArray(sample) ? samples.push(...sample) : samples.push(sample);
} catch (error) {
console.error(`Failed to parse getting started samples: ${error}`);
}
}
}
}

for (const sample of samples) {
if (!sample.icon) {
sample.icon = DEFAULT_ICON;
}
}

return samples;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type CoreV1API = Pick<
| 'readNamespacedSecret'
| 'replaceNamespacedSecret'
| 'deleteNamespacedSecret'
| 'listNamespacedConfigMap'
>;

export function prepareCoreV1API(kc: k8s.KubeConfig): CoreV1API {
Expand All @@ -47,5 +48,7 @@ export function prepareCoreV1API(kc: k8s.KubeConfig): CoreV1API {
retryableExec(() => coreV1API.replaceNamespacedSecret(...args)),
deleteNamespacedSecret: (...args: Parameters<typeof coreV1API.deleteNamespacedSecret>) =>
retryableExec(() => coreV1API.deleteNamespacedSecret(...args)),
listNamespacedConfigMap: (...args: Parameters<typeof coreV1API.listNamespacedConfigMap>) =>
retryableExec(() => coreV1API.listNamespacedConfigMap(...args)),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,10 @@ export interface IWatcherService<T = Record<string, unknown>> {
*/
stopWatching(): void;
}

export interface IGettingStartedSampleApi {
/**
* Reads all the Getting Started Samples ConfigMaps.
*/
list(): Promise<Array<api.IGettingStartedSample>>;
}
29 changes: 29 additions & 0 deletions packages/dashboard-backend/src/routes/api/gettingStartedSample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2018-2023 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { FastifyInstance} from 'fastify';
import { baseApiPath } from '../../constants/config';
import { getSchema } from '../../services/helpers';
import { getDevWorkspaceClient } from './helpers/getDevWorkspaceClient';
import { getServiceAccountToken } from './helpers/getServiceAccountToken';

const tags = ['Getting Started Sample'];

export function registerGettingStartedSampleRoutes(instance: FastifyInstance) {
instance.register(async server => {
server.get(`${baseApiPath}/getting-started-sample`, getSchema({ tags }), async () => {
const token = getServiceAccountToken();
const { gettingStartedSampleApi } = getDevWorkspaceClient(token);
return gettingStartedSampleApi.list();
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ export class SampleCard extends React.PureComponent<Props, State> {
'data-testid': 'sample-card-icon',
};

return metadata.icon ? <Brand src={metadata.icon} {...props} /> : <CubesIcon {...props} />;
return metadata.icon ? (
<Brand
src={`${
typeof metadata.icon === 'string'
? metadata.icon
: 'data:' + metadata.icon.mediatype + ';base64,' + metadata.icon.base64data
}`}
{...props}
/>
) : (
<CubesIcon {...props} />
);
}
}
10 changes: 10 additions & 0 deletions packages/dashboard-frontend/src/services/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ export default class Bootstrap {
undefined,
);

const gettingStartedSampleURL = new URL(
'/dashboard/api/getting-started-sample',
window.location.origin,
).href;
await requestRegistriesMetadata(gettingStartedSampleURL, false)(
this.store.dispatch,
this.store.getState,
undefined,
);

const serverConfig = this.store.getState().dwServerConfig.config;
const devfileRegistry = serverConfig.devfileRegistry;
const internalDevfileRegistryUrl = serverConfig.devfileRegistryURL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,31 @@ describe('fetch registry metadata', () => {
expect(resolved).toEqual([metadata]);
});

describe('getting started samples', () => {
const baseUrl = 'http://this.is.my.base.url';

it('should fetch getting started samples', async () => {
const metadata = {
displayName: 'java-maven',
tags: ['Java'],
url: 'some-url',
icon: { mediatype: 'image/png', base64data: 'some-data' },
} as che.DevfileMetaData;
mockFetchData.mockResolvedValue([metadata]);

const resolved = await fetchRegistryMetadata(
`${baseUrl}/dashboard/api/getting-started-sample`,
false,
);

expect(mockSessionStorageServiceGet).not.toHaveBeenCalled();
expect(mockFetchData).toHaveBeenCalledTimes(1);
expect(mockFetchData).toBeCalledWith(`${baseUrl}/dashboard/api/getting-started-sample`);
expect(mockSessionStorageServiceUpdate).not.toHaveBeenCalled();
expect(resolved).toEqual([metadata]);
});
});

it('should throw an error if fetched data is not array', async () => {
mockDateNow.mockReturnValue(1555555555555);
mockFetchData.mockResolvedValue('foo');
Expand Down
Loading

0 comments on commit 89c22da

Please sign in to comment.