Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow to devfile metadata via ConfigMap #912

Merged
merged 5 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { registerGettingStartedSamplesRoutes } 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);

registerGettingStartedSamplesRoutes(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 { GettingStartedSamplesApiService } 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(GettingStartedSamplesApiService);
});
});
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 { GettingStartedSamplesApiService } 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 GettingStartedSamplesApiService(this.kubeConfig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { GettingStartedSamplesApiService } from '../gettingStartedSamplesApi';
import { api } from '@eclipse-che/common';

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

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

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

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

gettingStartedSample = new GettingStartedSamplesApiService(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,79 @@
/*
* 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';
import { getIcon } from './helpers/getSampleIcon';
import http from 'http';
import { V1ConfigMapList } from '@kubernetes/client-node/dist/gen/model/v1ConfigMapList';

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';

export class GettingStartedSamplesApiService 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: { response: http.IncomingMessage; body: V1ConfigMapList };
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: api.IGettingStartedSample[] = [];

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}`);
}
}
}
}

// Ensure icon for each sample
samples.forEach(sample => (sample.icon = getIcon(sample)));

return samples;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { api } from '@eclipse-che/common';

const DEFAULT_ICON = {
base64data:
'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgaGVpZ2h0PSI1ZW0iIHdpZHRoPSI1ZW0iIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8ZyBmaWxsPSIjNmE2ZTczIj4KICA8cGF0aAogICAgICBkPSJNNDg4LjYgMjUwLjJMMzkyIDIxNFYxMDUuNWMwLTE1LTkuMy0yOC40LTIzLjQtMzMuN2wtMTAwLTM3LjVjLTguMS0zLjEtMTcuMS0zLjEtMjUuMyAwbC0xMDAgMzcuNWMtMTQuMSA1LjMtMjMuNCAxOC43LTIzLjQgMzMuN1YyMTRsLTk2LjYgMzYuMkM5LjMgMjU1LjUgMCAyNjguOSAwIDI4My45VjM5NGMwIDEzLjYgNy43IDI2LjEgMTkuOSAzMi4ybDEwMCA1MGMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAzLjktNTIgMTAzLjkgNTJjMTAuMSA1LjEgMjIuMSA1LjEgMzIuMiAwbDEwMC01MGMxMi4yLTYuMSAxOS45LTE4LjYgMTkuOS0zMi4yVjI4My45YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43ek0zNTggMjE0LjhsLTg1IDMxLjl2LTY4LjJsODUtMzd2NzMuM3pNMTU0IDEwNC4xbDEwMi0zOC4yIDEwMiAzOC4ydi42bC0xMDIgNDEuNC0xMDItNDEuNHYtLjZ6bTg0IDI5MS4xbC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnptMjQwIDExMmwtODUgNDIuNXYtNzkuMWw4NS0zOC44djc1LjR6bTAtMTEybC0xMDIgNDEuNC0xMDItNDEuNHYtLjZsMTAyLTM4LjIgMTAyIDM4LjJ2LjZ6Ij48L3BhdGg+CiAgPC9nPgo8L3N2Zz4K',
mediatype: 'image/svg+xml',
};

export function getIcon(sample: api.IGettingStartedSample) {
if (!sample?.icon) {
return DEFAULT_ICON;
}
return sample.icon;
}
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>>;
}
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 Samples'];

export function registerGettingStartedSamplesRoutes(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
Loading