Skip to content

Commit

Permalink
Add health check page
Browse files Browse the repository at this point in the history
  • Loading branch information
vikram-raj committed Apr 15, 2020
1 parent eeb2e83 commit 101e0c2
Show file tree
Hide file tree
Showing 12 changed files with 371 additions and 31 deletions.
16 changes: 16 additions & 0 deletions frontend/packages/dev-console/src/actions/modify-application.ts
Expand Up @@ -41,3 +41,19 @@ export const EditApplication = (model: K8sKind, obj: K8sResourceKind): KebabOpti
},
};
};

export const EditHealthCheck = (model: K8sKind, obj: K8sResourceKind): KebabOption => {
return {
label: 'Edit Health Checks',
href: `/k8s/ns/${obj.metadata.namespace}/${obj.kind || model.kind}/${
obj.metadata.name
}/containers/${obj.spec.template.spec.containers[0].name}/health-checks`,
accessReview: {
group: model.apiGroup,
resource: model.plural,
name: obj.metadata.name,
namespace: obj.metadata.namespace,
verb: 'update',
},
};
};
@@ -0,0 +1,65 @@
import * as React from 'react';
import * as _ from 'lodash';
import { FirehoseResult, ContainerDropdown, history } from '@console/internal/components/utils';
import { K8sResourceKind } from '@console/internal/module/k8s';
import HealthChecks from './HealthChecks';
import { FormikProps, FormikValues, useFormikContext } from 'formik';
import { Form } from '@patternfly/react-core';
import { FormFooter } from '@console/shared';

type AddHelathProps = {
resource?: FirehoseResult<K8sResourceKind>;
currentContainer: string;
};

const AddHealthCheck: React.FC<FormikProps<FormikValues> & AddHelathProps> = ({
resource,
currentContainer,
handleSubmit,
handleReset,
dirty,
errors,
status,
isSubmitting,
}) => {
const { data } = resource;
const [currentKey, setCurrentKey] = React.useState(currentContainer);
const { setFieldValue } = useFormikContext<FormikValues>();

const handleSelectContainer = (containerName) => {
setCurrentKey(containerName);
setFieldValue('containerName', containerName);
history.push(
`/k8s/ns/${data.metadata.namespace}/${data.kind || data.kind}/${
data.metadata.name
}/containers/${containerName}/health-checks`,
);
};

const containers = resource.loaded && _.keyBy(data.spec.template.spec.containers, 'name');
return (
<div className="co-m-pane__body">
<Form onSubmit={handleSubmit}>
<div>
Managing health check for:{' '}
<ContainerDropdown
currentKey={currentKey}
containers={containers}
onChange={handleSelectContainer}
/>
</div>
<HealthChecks />
<FormFooter
handleReset={handleReset}
errorMessage={status && `${status.errors}`}
isSubmitting={isSubmitting}
submitLabel="Save"
disableSubmit={!dirty || !_.isEmpty(errors)}
resetLabel="Cancel"
/>
</Form>
</div>
);
};

export default AddHealthCheck;
@@ -0,0 +1,151 @@
import * as React from 'react';
import * as _ from 'lodash';
import Helmet from 'react-helmet';
import { Formik } from 'formik';
import { RouteComponentProps } from 'react-router-dom';
import {
PageHeading,
FirehoseResource,
Firehose,
FirehoseResult,
LoadingBox,
StatusBox,
history,
} from '@console/internal/components/utils';
import { K8sResourceKind, k8sUpdate, referenceFor, modelFor } from '@console/internal/module/k8s';
import AddHealthCheck from './AddHealthCheck';
import { gethealthCheckProbeInitialData } from './health-check-probe-utils';
import { HealthCheckProbeData, RequestType } from './health-checks-types';
import { healthChecksProbesValidationSchema } from './health-check-probe-validation-utils';

type AddHealthCheckProps = RouteComponentProps<{
ns: string;
kind: string;
name: string;
containerName: string;
}>;

type HealthCheckWrapperProps = {
resource?: FirehoseResult<K8sResourceKind>;
currentContainer: string;
};

const constructProbeData = (data: HealthCheckProbeData) => {
const probeData = {
...(data.failureThreshold && { failureThreshold: data.failureThreshold }),
...(data.successThreshold && { successThreshold: data.successThreshold }),
...(data.requestType === RequestType.ContainerCommand && {
exec: data.exec,
}),
...(data.requestType === RequestType.HTTPGET && {
httpGet: {
path: data.httpGet.path,
...(_.includes(data[data.requestType]?.scheme, 'HTTPS') && {
scheme: 'HTTPS',
}),
port: _.toInteger(data.httpGet.port),
},
}),
...(data.requestType === RequestType.TCPSocket && {
tcpSocket: { port: _.toInteger(data.tcpSocket.port) },
}),
...(data.initialDelaySeconds && {
initialDelaySeconds: data.initialDelaySeconds,
}),
...(data.periodSeconds && { periodSeconds: data.periodSeconds }),
...(data.timeoutSeconds && { timeoutSeconds: data.timeoutSeconds }),
};
return probeData;
};

const HealthCheckWrapper: React.FC<HealthCheckWrapperProps> = ({ resource, currentContainer }) => {
if (!resource.loaded && _.isEmpty(resource.loadError)) {
return <LoadingBox />;
}

if (resource.loadError) {
return <StatusBox loaded={resource.loaded} loadError={resource.loadError} />;
}

const container = _.filter(
resource.data.spec.template.spec.containers,
(data) => data.name === currentContainer,
);

if (_.isEmpty(container)) {
return <div className="text-center">Container not found</div>;
}

const handleSubmit = (values, actions) => {
const {
healthChecks: { readinessProbe, livenessProbe, startupProbe },
containerName,
} = values;
const updatedResource = _.cloneDeep(resource.data);
const containerIndex = _.findIndex(updatedResource.spec.template.spec.containers, [
'name',
containerName,
]);
updatedResource.spec.template.spec.containers[containerIndex] = {
...container[containerIndex],
...(readinessProbe.enabled
? { readinessProbe: constructProbeData(readinessProbe.data) }
: {}),
...(livenessProbe.enabled ? { livenessProbe: constructProbeData(livenessProbe.data) } : {}),
...(startupProbe.enabled ? { startupProbe: constructProbeData(startupProbe.data) } : {}),
};
k8sUpdate(modelFor(referenceFor(resource.data)), updatedResource)
.then(() => {
actions.setSubmitting(false);
actions.setStatus({ error: '' });
history.push(`/topology/ns/${updatedResource.metadata.namespace}`);
})
.catch((err) => {
actions.setSubmitting(false);
actions.setStatus({ errors: err });
});
};
const initialValues = gethealthCheckProbeInitialData(resource.data);
initialValues.containerName = container[0].name;

return (
<Formik
initialValues={initialValues}
validationSchema={healthChecksProbesValidationSchema}
onSubmit={handleSubmit}
onReset={history.goBack}
>
{(props) => (
<AddHealthCheck resource={resource} currentContainer={currentContainer} {...props} />
)}
</Formik>
);
};

const AddHealthCheckPage: React.FC<AddHealthCheckProps> = ({ match }) => {
const { ns, kind, name, containerName } = match.params;
const resource: FirehoseResource[] = [
{
kind,
namespace: ns,
isList: false,
name,
prop: 'resource',
},
];
return (
<>
<Helmet>
<title>Health Checks</title>
</Helmet>
<div>
<PageHeading title="Health Checks" />
<Firehose resources={resource}>
<HealthCheckWrapper currentContainer={containerName} />
</Firehose>
</div>
</>
);
};

export default AddHealthCheckPage;
Expand Up @@ -23,6 +23,7 @@ const HealthCheckProbe: React.FC<HealthCheckProbeProps> = ({ probeType }) => {

const handleDeleteProbe = () => {
setFieldValue(`healthChecks.${probeType}`, initialValues.healthChecks[probeType]);
setFieldValue(`healthChecks.${probeType}.enabled`, false);
};

const handleReset = () => {
Expand Down
@@ -1,4 +1,6 @@
import * as _ from 'lodash';
import { HealthChecksProbeType, RequestType, HealthCheckProbe } from './health-checks-types';
import { K8sResourceKind } from '@console/internal/module/k8s';

export const getHealthChecksProbeConfig = (probe: string) => {
switch (probe) {
Expand Down Expand Up @@ -28,33 +30,58 @@ export const getHealthChecksProbeConfig = (probe: string) => {
}
};

export const defaultHealthChecksProbeValues: HealthCheckProbe = {
showForm: false,
enabled: false,
data: {
failureThreshold: 3,
requestType: RequestType.HTTPGET,
httpGet: {
scheme: 'HTTP',
path: '/',
port: 8080,
httpHeaders: [],
},
tcpSocket: {
port: 8080,
const getProbeType = (resourceProbe) => {
return resourceProbe?.hasOwnProperty('httpGet')
? RequestType.HTTPGET
: resourceProbe?.hasOwnProperty('exec')
? RequestType.ContainerCommand
: resourceProbe?.hasOwnProperty('tcpSocket')
? RequestType.TCPSocket
: RequestType.HTTPGET;
};

export const defaultHealthChecksProbeValues = (
resource: K8sResourceKind,
probeType: HealthChecksProbeType,
): HealthCheckProbe => {
const resourceContainer = resource?.spec?.template?.spec?.containers[0];
const resourceProbe = resourceContainer[probeType];
return {
showForm: false,
enabled: _.has(resourceContainer, probeType),
data: {
failureThreshold: resourceProbe?.failureThreshold || 3,
requestType: getProbeType(resourceProbe),
httpGet: {
scheme: [resourceProbe?.httpGet?.scheme] || ['HTTP'],
path: resourceProbe?.httpGet?.path || '/',
port: resourceProbe?.httpGet?.port || 8080,
httpHeaders: resourceProbe?.httpGet?.httpHeaders || [],
},
tcpSocket: {
port: resourceProbe?.tcpSocket?.port || 8080,
},
exec: {
command: resourceProbe?.exec?.command || [''],
},
initialDelaySeconds: resourceProbe?.initialDelaySeconds || 0,
periodSeconds: resourceProbe?.periodSeconds || 10,
timeoutSeconds: resourceProbe?.timeoutSeconds || 1,
successThreshold: resourceProbe?.successThreshold || 1,
},
exec: { command: [''] },
initialDelaySeconds: 0,
periodSeconds: 10,
timeoutSeconds: 1,
successThreshold: 1,
},
};
};

export const healthChecksProbeInitialData = {
healthChecks: {
readinessProbe: defaultHealthChecksProbeValues,
livenessProbe: defaultHealthChecksProbeValues,
startupProbe: defaultHealthChecksProbeValues,
},
export const gethealthCheckProbeInitialData = (resource: K8sResourceKind) => {
return {
containerName: '',
healthChecks: {
readinessProbe: defaultHealthChecksProbeValues(
resource,
HealthChecksProbeType.ReadinessProbe,
),
livenessProbe: defaultHealthChecksProbeValues(resource, HealthChecksProbeType.LivenessProbe),
startupProbe: defaultHealthChecksProbeValues(resource, HealthChecksProbeType.StartupProbe),
},
};
};
Expand Up @@ -3,6 +3,7 @@ import * as yup from 'yup';
const pathRegex = /^\/.*$/;

export const healthChecksValidationSchema = yup.object().shape({
containerName: yup.string(),
showForm: yup.boolean(),
enabled: yup.boolean(),
data: yup.object().when('showForm', {
Expand Down Expand Up @@ -42,3 +43,9 @@ export const healthChecksValidationSchema = yup.object().shape({
}),
}),
});

export const healthChecksProbesValidationSchema = yup.object().shape({
readinessProbe: healthChecksValidationSchema,
livenessProbe: healthChecksValidationSchema,
startupProbe: healthChecksValidationSchema,
});
Expand Up @@ -16,7 +16,7 @@ export interface HealthCheckProbeData {
failureThreshold: number;
requestType?: string;
httpGet?: {
scheme: string;
scheme: string[];
path: string;

port: number;
Expand Down
13 changes: 13 additions & 0 deletions frontend/packages/dev-console/src/plugin.tsx
Expand Up @@ -666,6 +666,19 @@ const plugin: Plugin<ConsumedExtensions> = [
).default,
},
},
{
type: 'Page/Route',
properties: {
exact: true,
path: ['/k8s/ns/:ns/:kind/:name/containers/:containerName/health-checks'],
loader: async () =>
(
await import(
'./components/health-checks/AddHealthCheckPage' /* webpackChunkName: "dev-console-healthCheck" */
)
).default,
},
},
{
type: 'ReduxReducer',
properties: {
Expand Down

0 comments on commit 101e0c2

Please sign in to comment.