Skip to content

Commit 3b9c842

Browse files
committed
feat: add DeploymentConfig support for OpenShift
The snyk-monitor will do a best-effort attempt to set up a Watch/Informer for DeploymentConfigs in the apps.openshift.io/v1 API. If this does not succeed, scanning will proceed as normal while printing a warning.
1 parent eae1eb7 commit 3b9c842

File tree

12 files changed

+284
-23
lines changed

12 files changed

+284
-23
lines changed

snyk-monitor/templates/clusterrole.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ rules:
5353
- get
5454
- list
5555
- watch
56+
- apiGroups:
57+
- apps.openshift.io
58+
resources:
59+
- deploymentconfigs
60+
verbs:
61+
- get
62+
- list
63+
- watch
5664
{{- if .Values.psp.enabled }}
5765
- apiGroups:
5866
- policy

snyk-monitor/templates/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ rules:
5151
- get
5252
- list
5353
- watch
54+
- apiGroups:
55+
- apps.openshift.io
56+
resources:
57+
- deploymentconfigs
58+
verbs:
59+
- get
60+
- list
61+
- watch
5462
{{- if .Values.psp.enabled }}
5563
- apiGroups:
5664
- policy

snyk-operator/deploy/olm-catalog/snyk-operator/0.0.0/snyk-operator.v0.0.0.clusterserviceversion.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ spec:
221221
- "*"
222222
verbs:
223223
- "*"
224+
- apiGroups:
225+
- apps.openshift.io
226+
resources:
227+
- deploymentconfigs
228+
verbs:
229+
- "*"
224230
serviceAccountName: snyk-operator
225231
deployments:
226232
- name: snyk-operator

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ function cleanUpTempStorage() {
4747
}
4848
};
4949

50-
function monitor(): void {
50+
async function monitor(): Promise<void> {
5151
try {
5252
logger.info({cluster: currentClusterName}, 'starting to monitor');
53-
beginWatchingWorkloads();
53+
await beginWatchingWorkloads();
5454
} catch (error) {
5555
logger.error({error}, 'an error occurred while monitoring the cluster');
5656
process.exit(1);
@@ -63,5 +63,5 @@ cleanUpTempStorage();
6363
// Allow running in an async context
6464
setImmediate(async function setUpAndMonitor(): Promise<void> {
6565
await loadAndSendWorkloadAutoImportPolicy();
66-
monitor();
66+
await monitor();
6767
});

src/supervisor/types.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { IncomingMessage } from 'http';
2-
import { AppsV1Api, BatchV1Api, BatchV1beta1Api, CoreV1Api, KubeConfig,
3-
V1ObjectMeta, V1OwnerReference, V1PodSpec } from '@kubernetes/client-node';
1+
import { IncomingMessage } from 'http';
2+
import {
3+
AppsV1Api,
4+
BatchV1Api,
5+
BatchV1beta1Api,
6+
CoreV1Api,
7+
CustomObjectsApi,
8+
KubeConfig,
9+
V1ObjectMeta,
10+
V1OwnerReference,
11+
V1PodSpec,
12+
} from '@kubernetes/client-node';
413

514
export enum WorkloadKind {
615
Deployment = 'Deployment',
@@ -11,6 +20,7 @@ export enum WorkloadKind {
1120
CronJob = 'CronJob',
1221
ReplicationController = 'ReplicationController',
1322
Pod = 'Pod',
23+
DeploymentConfig = 'DeploymentConfig',
1424
}
1525

1626
export interface IRequestError {
@@ -32,6 +42,7 @@ export interface IK8sClients {
3242
readonly coreClient: CoreV1Api;
3343
readonly batchClient: BatchV1Api;
3444
readonly batchUnstableClient: BatchV1beta1Api;
45+
readonly customObjectsClient: CustomObjectsApi;
3546
}
3647

3748
export class K8sClients implements IK8sClients {
@@ -41,12 +52,17 @@ export class K8sClients implements IK8sClients {
4152
// TODO: Keep an eye on this! We need v1beta1 API for CronJobs.
4253
// https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning
4354
// CronJobs will appear in v2 API, but for now there' only v2alpha1, so it's a bad idea to use it.
55+
// TODO: https://kubernetes.io/blog/2021/04/09/kubernetes-release-1.21-cronjob-ga/
56+
// CronJobs are now GA in Kubernetes 1.21 in the batch/v1 API, we should add support for it!
4457
public readonly batchUnstableClient: BatchV1beta1Api;
58+
/** This client is used to access Custom Resources in the cluster, e.g. DeploymentConfig on OpenShift. */
59+
public readonly customObjectsClient: CustomObjectsApi;
4560

4661
constructor(config: KubeConfig) {
4762
this.appsClient = config.makeApiClient(AppsV1Api);
4863
this.coreClient = config.makeApiClient(CoreV1Api);
4964
this.batchClient = config.makeApiClient(BatchV1Api);
5065
this.batchUnstableClient = config.makeApiClient(BatchV1beta1Api);
66+
this.customObjectsClient = config.makeApiClient(CustomObjectsApi);
5167
}
5268
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { deleteWorkload } from './workload';
2+
import { WorkloadKind } from '../../types';
3+
import { FALSY_WORKLOAD_NAME_MARKER, V1DeploymentConfig } from './types';
4+
5+
export async function deploymentConfigWatchHandler(
6+
deploymentConfig: V1DeploymentConfig,
7+
): Promise<void> {
8+
if (
9+
!deploymentConfig.metadata ||
10+
!deploymentConfig.spec ||
11+
!deploymentConfig.spec.template.metadata ||
12+
!deploymentConfig.spec.template.spec ||
13+
!deploymentConfig.status
14+
) {
15+
return;
16+
}
17+
18+
const workloadName =
19+
deploymentConfig.metadata.name || FALSY_WORKLOAD_NAME_MARKER;
20+
21+
await deleteWorkload(
22+
{
23+
kind: WorkloadKind.DeploymentConfig,
24+
objectMeta: deploymentConfig.metadata,
25+
specMeta: deploymentConfig.spec.template.metadata,
26+
ownerRefs: deploymentConfig.metadata.ownerReferences,
27+
revision: deploymentConfig.status.observedGeneration,
28+
podSpec: deploymentConfig.spec.template.spec,
29+
},
30+
workloadName,
31+
);
32+
}

src/supervisor/watchers/handlers/index.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { makeInformer, ADD, DELETE, ERROR, UPDATE, KubernetesObject } from '@kubernetes/client-node';
2+
23
import { logger } from '../../../common/logger';
34
import { WorkloadKind } from '../../types';
45
import { podWatchHandler, podDeletedHandler } from './pod';
@@ -9,6 +10,7 @@ import { jobWatchHandler } from './job';
910
import { replicaSetWatchHandler } from './replica-set';
1011
import { replicationControllerWatchHandler } from './replication-controller';
1112
import { statefulSetWatchHandler } from './stateful-set';
13+
import { deploymentConfigWatchHandler } from './deployment-config';
1214
import { k8sApi, kubeConfig } from '../../cluster';
1315
import * as kubernetesApiWrappers from '../../kuberenetes-api-wrappers';
1416
import { IWorkloadWatchMetadata, FALSY_WORKLOAD_NAME_MARKER } from './types';
@@ -91,9 +93,74 @@ const workloadWatchMetadata: Readonly<IWorkloadWatchMetadata> = {
9193
},
9294
listFactory: (namespace) => () => k8sApi.appsClient.listNamespacedStatefulSet(namespace),
9395
},
96+
[WorkloadKind.DeploymentConfig]: {
97+
/** https://docs.openshift.com/container-platform/4.7/rest_api/workloads_apis/deploymentconfig-apps-openshift-io-v1.html */
98+
endpoint: '/apis/apps.openshift.io/v1/watch/namespaces/{namespace}/deploymentconfigs',
99+
handlers: {
100+
[DELETE]: deploymentConfigWatchHandler,
101+
},
102+
listFactory: (namespace) => () => k8sApi.customObjectsClient.listNamespacedCustomObject('apps.openshift.io', 'v1', namespace, 'deploymentconfigs'),
103+
},
94104
};
95105

96-
export function setupInformer(namespace: string, workloadKind: WorkloadKind) {
106+
async function isSupportedWorkload(
107+
namespace: string,
108+
workloadKind: WorkloadKind,
109+
): Promise<boolean> {
110+
if (workloadKind !== WorkloadKind.DeploymentConfig) {
111+
return true;
112+
}
113+
114+
try {
115+
const pretty = undefined;
116+
const continueToken = undefined;
117+
const fieldSelector = undefined;
118+
const labelSelector = undefined;
119+
const limit = 1; // Try to grab only a single object
120+
const resourceVersion = undefined; // List anything in the cluster
121+
const timeoutSeconds = 10; // Don't block the snyk-monitor indefinitely
122+
const attemptedApiCall = await kubernetesApiWrappers.retryKubernetesApiRequest(
123+
() =>
124+
k8sApi.customObjectsClient.listNamespacedCustomObject(
125+
'apps.openshift.io',
126+
'v1',
127+
namespace,
128+
'deploymentconfigs',
129+
pretty,
130+
continueToken,
131+
fieldSelector,
132+
labelSelector,
133+
limit,
134+
resourceVersion,
135+
timeoutSeconds,
136+
),
137+
);
138+
return (
139+
attemptedApiCall !== undefined &&
140+
attemptedApiCall.response !== undefined &&
141+
attemptedApiCall.response.statusCode !== undefined &&
142+
attemptedApiCall.response.statusCode >= 200 &&
143+
attemptedApiCall.response.statusCode < 300
144+
);
145+
} catch (error) {
146+
logger.info(
147+
{ error, workloadKind },
148+
'Failed on Kubernetes API call to list DeploymentConfig',
149+
);
150+
return false;
151+
}
152+
}
153+
154+
export async function setupInformer(namespace: string, workloadKind: WorkloadKind): Promise<void> {
155+
const isSupported = await isSupportedWorkload(namespace, workloadKind);
156+
if (!isSupported) {
157+
logger.info(
158+
{ namespace, workloadKind },
159+
'The Kubernetes cluster does not support this workload',
160+
);
161+
return;
162+
}
163+
97164
const workloadMetadata = workloadWatchMetadata[workloadKind];
98165
const namespacedEndpoint = workloadMetadata.endpoint.replace('{namespace}', namespace);
99166

@@ -103,7 +170,10 @@ export function setupInformer(namespace: string, workloadKind: WorkloadKind) {
103170
return await kubernetesApiWrappers.retryKubernetesApiRequest(
104171
() => listMethod());
105172
} catch (err) {
106-
logger.error({err, namespace, workloadKind}, 'error while listing entities on namespace');
173+
logger.error(
174+
{ err, namespace, workloadKind },
175+
'error while listing entities on namespace',
176+
);
107177
throw err;
108178
}
109179
};
@@ -113,11 +183,11 @@ export function setupInformer(namespace: string, workloadKind: WorkloadKind) {
113183
informer.on(ERROR, (err) => {
114184
// Types from client library insists that callback is of type KubernetesObject
115185
if ((err as any).code === ECONNRESET_ERROR_CODE) {
116-
logger.debug(`informer ${ECONNRESET_ERROR_CODE} occurred, restarting informer`);
186+
logger.debug({}, `informer ${ECONNRESET_ERROR_CODE} occurred, restarting informer`);
117187

118188
// Restart informer after 1sec
119-
setTimeout(() => {
120-
informer.start();
189+
setTimeout(async () => {
190+
await informer.start();
121191
}, 1000);
122192
} else {
123193
logger.error({ err }, 'unexpected informer error event occurred');
@@ -135,5 +205,5 @@ export function setupInformer(namespace: string, workloadKind: WorkloadKind) {
135205
});
136206
}
137207

138-
informer.start();
208+
await informer.start();
139209
}

src/supervisor/watchers/handlers/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { V1ObjectMeta, V1PodTemplateSpec } from "@kubernetes/client-node";
2+
13
export const FALSY_WORKLOAD_NAME_MARKER = 'falsy workload name';
24

35
type WorkloadHandlerFunc = (workload: any) => Promise<void>;
@@ -16,3 +18,19 @@ export interface IWorkloadWatchMetadata {
1618
listFactory: ListWorkloadFunctionFactory;
1719
};
1820
}
21+
22+
export interface V1DeploymentConfig {
23+
apiVersion?: string;
24+
kind?: string;
25+
metadata?: V1ObjectMeta;
26+
spec?: V1DeploymentConfigSpec;
27+
status?: V1DeploymentConfigStatus;
28+
}
29+
30+
export interface V1DeploymentConfigSpec {
31+
template: V1PodTemplateSpec;
32+
}
33+
34+
export interface V1DeploymentConfigStatus {
35+
observedGeneration?: number;
36+
}

src/supervisor/watchers/index.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { kubernetesInternalNamespaces } from './internal-namespaces';
1818
*/
1919
const watchedNamespaces = new Set<string>();
2020

21-
function setupWatchesForNamespace(namespace: string): void {
21+
async function setupWatchesForNamespace(namespace: string): Promise<void> {
2222
if (watchedNamespaces.has(namespace)) {
2323
logger.info({namespace}, 'already set up namespace watch, skipping');
2424
return;
@@ -33,7 +33,7 @@ function setupWatchesForNamespace(namespace: string): void {
3333
}
3434

3535
try {
36-
setupInformer(namespace, workloadKind);
36+
await setupInformer(namespace, workloadKind);
3737
} catch (error) {
3838
logger.warn({namespace, workloadKind}, 'could not setup workload watch, skipping');
3939
}
@@ -53,7 +53,7 @@ export function isKubernetesInternalNamespace(namespace: string): boolean {
5353
return kubernetesInternalNamespaces.has(namespace);
5454
}
5555

56-
function setupWatchesForCluster(): void {
56+
async function setupWatchesForCluster(): Promise<void> {
5757
const informer = makeInformer(
5858
kubeConfig,
5959
'/api/v1/namespaces',
@@ -74,15 +74,15 @@ function setupWatchesForCluster(): void {
7474
logger.debug(`namespace informer ${ECONNRESET_ERROR_CODE} occurred, restarting informer`);
7575

7676
// Restart informer after 1sec
77-
setTimeout(() => {
78-
informer.start();
77+
setTimeout(async () => {
78+
await informer.start();
7979
}, 1000);
8080
} else {
8181
logger.error({ err }, 'unexpected namespace informer error event occurred');
8282
}
8383
});
8484

85-
informer.on(ADD, (namespace: V1Namespace) => {
85+
informer.on(ADD, async (namespace: V1Namespace) => {
8686
try {
8787
const namespaceName = extractNamespaceName(namespace);
8888
if (isKubernetesInternalNamespace(namespaceName)) {
@@ -91,23 +91,23 @@ function setupWatchesForCluster(): void {
9191
return;
9292
}
9393

94-
setupWatchesForNamespace(namespaceName);
94+
await setupWatchesForNamespace(namespaceName);
9595
} catch (err) {
9696
logger.error({err, namespace}, 'error handling a namespace event');
9797
return;
9898
}
9999
});
100100

101-
informer.start();
101+
await informer.start();
102102
}
103103

104-
export function beginWatchingWorkloads(): void {
104+
export async function beginWatchingWorkloads(): Promise<void> {
105105
if (config.NAMESPACE) {
106106
logger.info({namespace: config.NAMESPACE}, 'kubernetes-monitor restricted to specific namespace');
107-
setupWatchesForNamespace(config.NAMESPACE);
107+
await setupWatchesForNamespace(config.NAMESPACE);
108108
return;
109109
}
110110

111-
setupWatchesForCluster();
111+
await setupWatchesForCluster();
112112
}
113113

0 commit comments

Comments
 (0)