Skip to content

Commit 4b354db

Browse files
authored
Merge pull request #96 from snyk/feat/informer-api
Feat/informer api
2 parents efc624d + 266e48c commit 4b354db

File tree

17 files changed

+202
-152
lines changed

17 files changed

+202
-152
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"main": "dist/index.js",
55
"scripts": {
66
"pretest": "./scripts/build-image.sh",
7-
"test": "npm run lint && npm run test:unit && npm run test:integration",
7+
"test": "npm run lint && npm run build && npm run test:unit && npm run test:integration",
88
"test:unit": "./tap test/unit -R spec",
99
"test:integration": "./tap test/integration -Rdot --timeout=600",
1010
"test:coverage": "npm run test:unit -- --coverage",

snyk-monitor-cluster-permissions.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,24 @@ rules:
1111
resources:
1212
- pods
1313
verbs:
14+
- get
1415
- list
1516
- watch
1617
- apiGroups:
1718
- ""
1819
resources:
1920
- namespaces
2021
verbs:
22+
- get
23+
- list
2124
- watch
2225
- apiGroups:
2326
- ""
2427
resources:
2528
- replicationcontrollers
2629
verbs:
2730
- get
31+
- list
2832
- watch
2933
- apiGroups:
3034
- batch
@@ -33,6 +37,7 @@ rules:
3337
- jobs
3438
verbs:
3539
- get
40+
- list
3641
- watch
3742
- apiGroups:
3843
- apps
@@ -43,6 +48,7 @@ rules:
4348
- statefulsets
4449
verbs:
4550
- get
51+
- list
4652
- watch
4753
---
4854
kind: ServiceAccount

snyk-monitor-namespaced-permissions.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ rules:
1111
resources:
1212
- pods
1313
verbs:
14+
- get
1415
- list
1516
- watch
1617
- apiGroups:
@@ -19,6 +20,7 @@ rules:
1920
- replicationcontrollers
2021
verbs:
2122
- get
23+
- list
2224
- watch
2325
- apiGroups:
2426
- batch
@@ -27,6 +29,7 @@ rules:
2729
- jobs
2830
verbs:
2931
- get
32+
- list
3033
- watch
3134
- apiGroups:
3235
- apps
@@ -37,6 +40,7 @@ rules:
3740
- statefulsets
3841
verbs:
3942
- get
43+
- list
4044
- watch
4145
---
4246
kind: ServiceAccount

snyk-monitor/templates/clusterrole.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,24 @@ rules:
1414
resources:
1515
- pods
1616
verbs:
17+
- get
1718
- list
1819
- watch
1920
- apiGroups:
2021
- ""
2122
resources:
2223
- namespaces
2324
verbs:
25+
- get
26+
- list
2427
- watch
2528
- apiGroups:
2629
- ""
2730
resources:
2831
- replicationcontrollers
2932
verbs:
3033
- get
34+
- list
3135
- watch
3236
- apiGroups:
3337
- batch
@@ -36,6 +40,7 @@ rules:
3640
- jobs
3741
verbs:
3842
- get
43+
- list
3944
- watch
4045
- apiGroups:
4146
- apps
@@ -46,5 +51,6 @@ rules:
4651
- statefulsets
4752
verbs:
4853
- get
54+
- list
4955
- watch
5056
{{- end }}

snyk-monitor/templates/role.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ rules:
1414
resources:
1515
- pods
1616
verbs:
17+
- get
1718
- list
1819
- watch
1920
- apiGroups:
@@ -28,6 +29,7 @@ rules:
2829
- replicationcontrollers
2930
verbs:
3031
- get
32+
- list
3133
- watch
3234
- apiGroups:
3335
- batch
@@ -36,6 +38,7 @@ rules:
3638
- jobs
3739
verbs:
3840
- get
41+
- list
3942
- watch
4043
- apiGroups:
4144
- apps
@@ -46,5 +49,6 @@ rules:
4649
- statefulsets
4750
verbs:
4851
- get
52+
- list
4953
- watch
5054
{{- end }}

src/kube-scanner/watchers/handlers/cron-job.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { V1beta1CronJob } from '@kubernetes/client-node';
2-
import { WatchEventType } from '../types';
32
import { deleteWorkload } from './index';
43
import { WorkloadKind } from '../../types';
54
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
65

7-
export async function cronJobWatchHandler(eventType: string, cronJob: V1beta1CronJob) {
8-
if (eventType !== WatchEventType.Deleted) {
9-
return;
10-
}
11-
6+
export async function cronJobWatchHandler(cronJob: V1beta1CronJob) {
127
if (!cronJob.metadata || !cronJob.spec || !cronJob.spec.jobTemplate.spec ||
138
!cronJob.spec.jobTemplate.metadata || !cronJob.spec.jobTemplate.spec.template.spec) {
149
// TODO(ivanstanev): possibly log this. It shouldn't happen but we should track it!

src/kube-scanner/watchers/handlers/daemon-set.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { V1DaemonSet } from '@kubernetes/client-node';
2-
import { WatchEventType } from '../types';
32
import { deleteWorkload } from './index';
43
import { WorkloadKind } from '../../types';
54
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
65

7-
export async function daemonSetWatchHandler(eventType: string, daemonSet: V1DaemonSet) {
8-
if (eventType !== WatchEventType.Deleted) {
9-
return;
10-
}
11-
6+
export async function daemonSetWatchHandler(daemonSet: V1DaemonSet) {
127
if (!daemonSet.metadata || !daemonSet.spec || !daemonSet.spec.template.metadata ||
138
!daemonSet.spec.template.spec) {
149
// TODO(ivanstanev): possibly log this. It shouldn't happen but we should track it!

src/kube-scanner/watchers/handlers/deployment.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { V1Deployment } from '@kubernetes/client-node';
2-
import { WatchEventType } from '../types';
32
import { deleteWorkload } from './index';
43
import { WorkloadKind } from '../../types';
54
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
65

7-
export async function deploymentWatchHandler(eventType: string, deployment: V1Deployment) {
8-
if (eventType !== WatchEventType.Deleted) {
9-
return;
10-
}
11-
6+
export async function deploymentWatchHandler(deployment: V1Deployment) {
127
if (!deployment.metadata || !deployment.spec || !deployment.spec.template.metadata ||
138
!deployment.spec.template.spec) {
149
// TODO(ivanstanev): possibly log this. It shouldn't happen but we should track it!

src/kube-scanner/watchers/handlers/index.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,117 @@
1+
import { makeInformer, ADD, DELETE, UPDATE, KubernetesObject } from '@kubernetes/client-node';
12
import logger = require('../../../common/logger');
23
import WorkloadWorker = require('../../../kube-scanner');
34
import { buildWorkloadMetadata } from '../../metadata-extractor';
4-
import { KubeObjectMetadata } from '../../types';
5+
import { KubeObjectMetadata, WorkloadKind } from '../../types';
6+
import { podWatchHandler } from './pod';
7+
import { cronJobWatchHandler } from './cron-job';
8+
import { daemonSetWatchHandler } from './daemon-set';
9+
import { deploymentWatchHandler } from './deployment';
10+
import { jobWatchHandler } from './job';
11+
import { replicaSetWatchHandler } from './replica-set';
12+
import { replicationControllerWatchHandler } from './replication-controller';
13+
import { statefulSetWatchHandler } from './stateful-set';
14+
import { k8sApi, kubeConfig } from '../../cluster';
15+
import { IWorkloadWatchMetadata, FALSY_WORKLOAD_NAME_MARKER } from './types';
16+
17+
/**
18+
* This map is used in combination with the kubernetes-client Informer API
19+
* to abstract which resources to watch, what their endpoint is, how to grab
20+
* a list of the resources, and which watch actions to handle (e.g. a newly added resource).
21+
*
22+
* The Informer API is just a wrapper around Kubernetes watches that makes sure the watch
23+
* gets restarted if it dies and it also efficiently tracks changes to the watched workloads
24+
* by comparing their resourceVersion.
25+
*
26+
* The map is keyed by the "WorkloadKind" -- the type of resource we want to watch.
27+
* Legal verbs for the "handlers" are pulled from '@kubernetes/client-node'. You can
28+
* set a different handler for every verb.
29+
* (e.g. ADD-ed workloads are processed differently than DELETE-d ones)
30+
*
31+
* The "listFunc" is a callback used by the kubernetes-client to grab the watched resource
32+
* whenever Kubernetes fires a "workload changed" event and it uses the result to figure out
33+
* if the workload actually changed (by inspecting the resourceVersion).
34+
*/
35+
const workloadWatchMetadata: Readonly<IWorkloadWatchMetadata> = {
36+
[WorkloadKind.Pod]: {
37+
endpoint: '/api/v1/namespaces/{namespace}/pods',
38+
handlers: {
39+
[ADD]: podWatchHandler,
40+
[UPDATE]: podWatchHandler,
41+
},
42+
listFactory: (namespace) => () => k8sApi.coreClient.listNamespacedPod(namespace),
43+
},
44+
[WorkloadKind.ReplicationController]: {
45+
endpoint: '/api/v1/watch/namespaces/{namespace}/replicationcontrollers',
46+
handlers: {
47+
[DELETE]: replicationControllerWatchHandler,
48+
},
49+
listFactory: (namespace) => () => k8sApi.coreClient.listNamespacedReplicationController(namespace),
50+
},
51+
[WorkloadKind.CronJob]: {
52+
endpoint: '/apis/batch/v1beta1/watch/namespaces/{namespace}/cronjobs',
53+
handlers: {
54+
[DELETE]: cronJobWatchHandler,
55+
},
56+
listFactory: (namespace) => () => k8sApi.batchUnstableClient.listNamespacedCronJob(namespace),
57+
},
58+
[WorkloadKind.Job]: {
59+
endpoint: '/apis/batch/v1/watch/namespaces/{namespace}/jobs',
60+
handlers: {
61+
[DELETE]: jobWatchHandler,
62+
},
63+
listFactory: (namespace) => () => k8sApi.batchClient.listNamespacedJob(namespace),
64+
},
65+
[WorkloadKind.DaemonSet]: {
66+
endpoint: '/apis/apps/v1/watch/namespaces/{namespace}/daemonsets',
67+
handlers: {
68+
[DELETE]: daemonSetWatchHandler,
69+
},
70+
listFactory: (namespace) => () => k8sApi.appsClient.listNamespacedDaemonSet(namespace),
71+
},
72+
[WorkloadKind.Deployment]: {
73+
endpoint: '/apis/apps/v1/watch/namespaces/{namespace}/deployments',
74+
handlers: {
75+
[DELETE]: deploymentWatchHandler,
76+
},
77+
listFactory: (namespace) => () => k8sApi.appsClient.listNamespacedDeployment(namespace),
78+
},
79+
[WorkloadKind.ReplicaSet]: {
80+
endpoint: '/apis/apps/v1/watch/namespaces/{namespace}/replicasets',
81+
handlers: {
82+
[DELETE]: replicaSetWatchHandler,
83+
},
84+
listFactory: (namespace) => () => k8sApi.appsClient.listNamespacedReplicaSet(namespace),
85+
},
86+
[WorkloadKind.StatefulSet]: {
87+
endpoint: '/apis/apps/v1/watch/namespaces/{namespace}/statefulsets',
88+
handlers: {
89+
[DELETE]: statefulSetWatchHandler,
90+
},
91+
listFactory: (namespace) => () => k8sApi.appsClient.listNamespacedStatefulSet(namespace),
92+
},
93+
};
94+
95+
export function setupInformer(namespace: string, workloadKind: WorkloadKind) {
96+
const workloadMetadata = workloadWatchMetadata[workloadKind];
97+
const namespacedEndpoint = workloadMetadata.endpoint.replace('{namespace}', namespace);
98+
99+
const informer = makeInformer<KubernetesObject>(kubeConfig, namespacedEndpoint,
100+
workloadMetadata.listFactory(namespace));
101+
102+
for (const informerVerb of Object.keys(workloadMetadata.handlers)) {
103+
informer.on(informerVerb, async (watchedWorkload) => {
104+
try {
105+
await workloadMetadata.handlers[informerVerb](watchedWorkload);
106+
} catch (error) {
107+
const name = watchedWorkload.metadata && watchedWorkload.metadata.name || FALSY_WORKLOAD_NAME_MARKER;
108+
logger.warn({error, namespace, name, workloadKind}, 'could not execute the informer handler for a workload');
109+
}
110+
});
111+
}
112+
113+
informer.start();
114+
}
5115

6116
export async function deleteWorkload(kubernetesMetadata: KubeObjectMetadata, workloadName: string) {
7117
try {

src/kube-scanner/watchers/handlers/job.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { V1Job } from '@kubernetes/client-node';
2-
import { WatchEventType } from '../types';
32
import { deleteWorkload } from './index';
43
import { WorkloadKind } from '../../types';
54
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
65

7-
export async function jobWatchHandler(eventType: string, job: V1Job) {
8-
if (eventType !== WatchEventType.Deleted) {
9-
return;
10-
}
11-
6+
export async function jobWatchHandler(job: V1Job) {
127
if (!job.metadata || !job.spec || !job.spec.template.metadata || !job.spec.template.spec) {
138
// TODO(ivanstanev): possibly log this. It shouldn't happen but we should track it!
149
return;

0 commit comments

Comments
 (0)