Skip to content

Commit

Permalink
Fix unnecessary redraws, add watch for enablement of ISVs
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed May 11, 2021
1 parent 131c4ae commit a39e96b
Show file tree
Hide file tree
Showing 12 changed files with 430 additions and 166 deletions.
4 changes: 2 additions & 2 deletions backend/src/routes/api/validate-isv/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { DEV_MODE } from '../../../utils/constants';
import { addCORSHeader } from '../../../utils/responseUtils';
import { createValidationJob } from './validateISV';
import { runValidation } from './validateISV';

export default async (fastify: FastifyInstance): Promise<void> => {
fastify.get('/', async (request: FastifyRequest, reply: FastifyReply) => {
return createValidationJob(fastify, request)
return runValidation(fastify, request)
.then((res) => {
if (DEV_MODE) {
addCORSHeader(request, reply);
Expand Down
125 changes: 91 additions & 34 deletions backend/src/routes/api/validate-isv/validateISV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@ import { FastifyRequest } from 'fastify';
import { KubeFastifyInstance, ODHApp } from '../../../types';
import { getApplicationDef } from '../../../utils/resourceUtils';

const doSleep = (timeout: number) => {
return new Promise((resolve) => setTimeout(resolve, timeout));
};

const waitOnDeletion = async (reader: () => Promise<void>) => {
const MAX_TRIES = 25;
let tries = 0;
let deleted = false;

while (!deleted && ++tries < MAX_TRIES) {
await reader()
.then(() => doSleep(1000))
.catch(() => {
deleted = true;
});
}
};

const waitOnCompletion = async (reader: () => Promise<boolean>): Promise<boolean> => {
const MAX_TRIES = 60;
let tries = 0;
let completionStatus;

while (completionStatus === undefined && ++tries < MAX_TRIES) {
await reader()
.then((res) => {
completionStatus = res;
})
.catch(async () => {
await doSleep(1000);
return;
});
}
return completionStatus || false;
};

export const createAccessSecret = async (
appDef: ODHApp,
namespace: string,
Expand Down Expand Up @@ -34,10 +70,10 @@ export const createAccessSecret = async (
});
};

export const createValidationJob = async (
export const runValidation = async (
fastify: KubeFastifyInstance,
request: FastifyRequest,
): Promise<{ response: IncomingMessage; body: V1Secret }> => {
): Promise<boolean> => {
const namespace = fastify.kube.namespace;
const query = request.query as { [key: string]: string };
const appName = query?.appName;
Expand All @@ -57,43 +93,64 @@ export const createValidationJob = async (
error.message = 'Unable to validate the application.';
throw error;
}
const jobName = `${cronjobName}-job-custom-run`;

await createAccessSecret(appDef, namespace, stringData, coreV1Api);

return createAccessSecret(appDef, namespace, stringData, coreV1Api).then(() => {
return batchV1beta1Api.readNamespacedCronJob(cronjobName, namespace).then(async (res) => {
const cronJob = res.body;
const jobSpec = cronJob.spec.jobTemplate.spec;
const jobName = `${cronjobName}-job-custom-run`;
const job = {
apiVersion: 'batch/v1',
metadata: {
name: jobName,
namespace,
annotations: {
'cronjob.kubernetes.io/instantiate': 'manual',
},
},
spec: jobSpec,
};
// Flag the cronjob as no longer suspended
cronJob.spec.suspend = false;
await batchV1beta1Api.replaceNamespacedCronJob(cronjobName, namespace, cronJob).catch((e) => {
fastify.log.error(`failed to unsuspend cronjob: ${e.response.body.message}`);
const cronJob = await batchV1beta1Api
.readNamespacedCronJob(cronjobName, namespace)
.then((res) => res.body);

// Flag the cronjob as no longer suspended
cronJob.spec.suspend = false;
await batchV1beta1Api.replaceNamespacedCronJob(cronjobName, namespace, cronJob).catch((e) => {
fastify.log.error(`failed to unsuspend cronjob: ${e.response.body.message}`);
});

// If there was a manual job already, delete it
await batchV1Api.deleteNamespacedJob(jobName, namespace).catch(() => {
return;
});

// wait for job to be deleted
await waitOnDeletion(() => {
return batchV1Api.readNamespacedJob(jobName, namespace).then(() => {
return;
});
});

// Wait for previous config map to be deleted
if (cmName) {
await waitOnDeletion(() => {
return coreV1Api.readNamespacedConfigMap(cmName, namespace).then(() => {
return;
});
});
}

// If there was a manual job already, delete it
// eslint-disable-next-line @typescript-eslint/no-empty-function
await batchV1Api.deleteNamespacedJob(jobName, namespace).catch(() => {});
const job = {
apiVersion: 'batch/v1',
metadata: {
name: jobName,
namespace,
annotations: {
'cronjob.kubernetes.io/instantiate': 'manual',
},
},
spec: cronJob.spec.jobTemplate.spec,
};

// If there was a config map already, delete it
if (cmName) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await coreV1Api.deleteNamespacedConfigMap(cmName, namespace).catch(() => {});
}
await batchV1Api.createNamespacedJob(namespace, job);

// Some delay to allow job to delete
return new Promise((resolve) => setTimeout(resolve, 1000)).then(() =>
batchV1Api.createNamespacedJob(namespace, job),
);
return await waitOnCompletion(() => {
return batchV1Api.readNamespacedJobStatus(jobName, namespace).then((res) => {
if (res.body.status.succeeded) {
return true;
}
if (res.body.status.failed) {
return false;
}
throw new Error();
});
});
};
33 changes: 17 additions & 16 deletions backend/src/utils/componentUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { IncomingMessage } from 'http';
import { V1ConfigMap } from '@kubernetes/client-node/dist/gen/model/v1ConfigMap';
import { ODHApp, K8sResourceCommon, KubeFastifyInstance, RouteKind } from '../types';

type RoutesResponse = {
body: {
items: RouteKind[];
};
response: IncomingMessage;
};

const getURLForRoute = (route: RouteKind, routeSuffix: string): string => {
const host = route?.spec?.host;
if (!host) {
Expand All @@ -21,14 +29,10 @@ export const getLink = async (
const customObjectsApi = fastify.kube.customObjectsApi;
const routeNamespace = namespace || fastify.kube.namespace;
try {
const res = await customObjectsApi.getNamespacedCustomObject(
'route.openshift.io',
'v1',
routeNamespace,
'routes',
routeName,
);
return getURLForRoute(res?.body as RouteKind, routeSuffix);
const route = await customObjectsApi
.getNamespacedCustomObject('route.openshift.io', 'v1', routeNamespace, 'routes', routeName)
.then((res) => res.body as RouteKind);
return getURLForRoute(route, routeSuffix);
} catch (e) {
fastify.log.error(`failed to get route ${routeName} in namespace ${namespace}`);
return null;
Expand All @@ -52,13 +56,10 @@ export const getServiceLink = async (
const customObjectsApi = fastify.kube.customObjectsApi;
const { namespace } = service.metadata;
try {
const res = await customObjectsApi.listNamespacedCustomObject(
'route.openshift.io',
'v1',
namespace,
'routes',
);
return getURLForRoute((res?.body as { items: RouteKind[] })?.items?.[0], routeSuffix);
const routes = await customObjectsApi
.listNamespacedCustomObject('route.openshift.io', 'v1', namespace, 'routes')
.then((res: RoutesResponse) => res.body?.items);
return getURLForRoute(routes?.[0], routeSuffix);
} catch (e) {
fastify.log.error(`failed to get route in namespace ${namespace}`);
return null;
Expand All @@ -77,7 +78,7 @@ export const getApplicationEnabledConfigMap = (
const coreV1Api = fastify.kube.coreV1Api;
return coreV1Api
.readNamespacedConfigMap(name, namespace)
.then((result: { body: V1ConfigMap }) => result.body)
.then((result) => result.body)
.catch(() => null);
};

Expand Down
1 change: 1 addition & 0 deletions frontend/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@typescript-eslint/no-inferrable-types": "error",
"react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "error",
"react/display-name": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"prettier/prettier": [
Expand Down
63 changes: 44 additions & 19 deletions frontend/src/pages/enabledApplications/EnabledApplications.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import * as React from 'react';
import * as _ from 'lodash';
import { Gallery, PageSection } from '@patternfly/react-core';
import { useWatchComponents } from '../../utilities/useWatchComponents';
import { ODHApp } from '../../types';
import ApplicationsPage from '../ApplicationsPage';
import OdhAppCard from '../../components/OdhAppCard';
import QuickStarts from '../../app/QuickStarts';
Expand All @@ -10,31 +12,54 @@ import './EnabledApplications.scss';
const description = `Launch your enabled applications or get started with quick start instructions
and tasks.`;

type EnabledApplicationsInnerProps = {
loaded: boolean;
loadError?: Error;
components: ODHApp[];
};
const EnabledApplicationsInner: React.FC<EnabledApplicationsInnerProps> = React.memo(
({ loaded, loadError, components }) => {
const isEmpty = !components || components.length === 0;
return (
<QuickStarts>
<ApplicationsPage
title="Enabled"
description={description}
loaded={loaded}
empty={isEmpty}
loadError={loadError}
>
{!isEmpty ? (
<PageSection>
<Gallery className="odh-installed-apps__gallery" hasGutter>
{components.map((c) => (
<OdhAppCard key={c.metadata.name} odhApp={c} />
))}
</Gallery>
</PageSection>
) : null}
</ApplicationsPage>
</QuickStarts>
);
},
);

const EnabledApplications: React.FC = () => {
const { components, loaded, loadError } = useWatchComponents(true);

const isEmpty = !components || components.length === 0;
const sortedComponents = React.useMemo(() => {
return _.cloneDeep(components).sort((a, b) =>
a.spec.displayName.localeCompare(b.spec.displayName),
);
}, [components]);

return (
<QuickStarts>
<ApplicationsPage
title="Enabled"
description={description}
<EnabledApplicationsInner
loaded={loaded}
empty={isEmpty}
components={sortedComponents}
loadError={loadError}
>
{!isEmpty ? (
<PageSection>
<Gallery className="odh-installed-apps__gallery" hasGutter>
{components
.sort((a, b) => a.spec.displayName.localeCompare(b.spec.displayName))
.map((c) => (
<OdhAppCard key={c.metadata.name} odhApp={c} />
))}
</Gallery>
</PageSection>
) : null}
</ApplicationsPage>
/>
</QuickStarts>
);
};
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/pages/exploreApplication/EnableModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.odh-enable-modal {
&__progress-title {
display: flex;
align-items: center;
> svg {
margin-right: var(--pf-global--spacer--sm);
}
}
.pf-c-alert.m-no-alert-icon {
.pf-c-alert__icon {
display: none;
}
}
&__variable-input {
max-width: calc(400px - var(--pf-global--spacer--md));
margin-right: var(--pf-global--spacer--xl);
}
&__toggle-password-vis {
border: 1px solid var(--pf-global--BorderColor--300);
border-radius: 0;
color: var(--pf-global--Color--200) !important;
padding: 6px var(--pf-global--spacer--sm) 5px;
position: relative;
right: 33px;
}
}
Loading

0 comments on commit a39e96b

Please sign in to comment.