Skip to content

Commit a87f909

Browse files
authored
Merge pull request #998 from snyk/feat/cluster-informers
Feat/cluster informers
2 parents 8dfe460 + 86c7542 commit a87f909

26 files changed

+1194
-424
lines changed

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929
"build": "tsc",
3030
"dev": "tsc-watch --project tsconfig.json --onSuccess 'node --inspect .'",
3131
"debug": "tsc-watch --project tsconfig.json --onSuccess 'node --inspect-brk .'",
32-
"lint": "npm run lint:commit && npm run lint:eslint",
32+
"lint": "npm run lint:commit && npm run lint:eslint && npm run lint:circular",
3333
"lint:eslint": "eslint \"src/**/*.ts\" && (cd test && eslint \"**/*.ts\")",
3434
"lint:commit": "commitlint --from=$(git rev-parse origin/staging)",
35+
"lint:circular": "fadge detect-cycles --allow-ignores=true \"./{src,test}/**/*.ts\"",
3536
"format": "prettier --write '{src,test}/**/*.{js,ts,json,yml}'",
3637
"format:check": "prettier --check '{src,test}/**/*.{js,ts,json,yml}'"
3738
},
@@ -67,6 +68,7 @@
6768
"eslint": "^7.32.0",
6869
"eslint-config-prettier": "^8.3.0",
6970
"eslint-plugin-prettier": "^3.4.1",
71+
"fadge": "^0.0.1",
7072
"jest": "^26.6.3",
7173
"nock": "^13.0.11",
7274
"prettier": "^2.4.1",

src/state.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { KubernetesObject, V1Namespace } from '@kubernetes/client-node';
22
import LruCache from 'lru-cache';
33

44
import { config } from './common/config';
5+
import { extractNamespaceName } from './supervisor/watchers/internal-namespaces';
56

67
const imagesLruCacheOptions: LruCache.Options<string, string> = {
78
// limit cache size so we don't exceed memory limit
@@ -113,6 +114,16 @@ export function kubernetesObjectToWorkloadAlreadyScanned(
113114
return undefined;
114115
}
115116

117+
export function storeNamespace(namespace: V1Namespace): void {
118+
const namespaceName = extractNamespaceName(namespace);
119+
state.watchedNamespaces[namespaceName] = namespace;
120+
}
121+
122+
export function deleteNamespace(namespace: V1Namespace): void {
123+
const namespaceName = extractNamespaceName(namespace);
124+
delete state.watchedNamespaces[namespaceName];
125+
}
126+
116127
export const state = {
117128
shutdownInProgress: false,
118129
imagesAlreadyScanned: new LruCache<string, string>(imagesLruCacheOptions),

src/supervisor/watchers/handlers/cron-job.ts

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@ import {
33
V1CronJobList,
44
V1beta1CronJob,
55
V1beta1CronJobList,
6+
BatchV1Api,
7+
BatchV1beta1Api,
68
} from '@kubernetes/client-node';
7-
import { deleteWorkload, trimWorkload } from './workload';
9+
import { deleteWorkload } from './workload';
810
import { WorkloadKind } from '../../types';
911
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
1012
import { IncomingMessage } from 'http';
1113
import { k8sApi } from '../../cluster';
12-
import { paginatedList } from './pagination';
14+
import { paginatedClusterList, paginatedNamespacedList } from './pagination';
1315
import {
1416
deleteWorkloadAlreadyScanned,
1517
deleteWorkloadImagesAlreadyScanned,
1618
kubernetesObjectToWorkloadAlreadyScanned,
1719
} from '../../../state';
20+
import { logger } from '../../../common/logger';
21+
import { retryKubernetesApiRequest } from '../../kuberenetes-api-wrappers';
22+
import { trimWorkload } from '../../workload-sanitization';
1823

19-
export async function paginatedCronJobList(namespace: string): Promise<{
24+
export async function paginatedNamespacedCronJobList(
25+
namespace: string,
26+
): Promise<{
2027
response: IncomingMessage;
2128
body: V1CronJobList;
2229
}> {
@@ -25,14 +32,31 @@ export async function paginatedCronJobList(namespace: string): Promise<{
2532
v1CronJobList.kind = 'CronJobList';
2633
v1CronJobList.items = new Array<V1CronJob>();
2734

28-
return await paginatedList(
35+
return await paginatedNamespacedList(
2936
namespace,
3037
v1CronJobList,
3138
k8sApi.batchClient.listNamespacedCronJob.bind(k8sApi.batchClient),
3239
);
3340
}
3441

35-
export async function paginatedCronJobV1Beta1List(namespace: string): Promise<{
42+
export async function paginatedClusterCronJobList(): Promise<{
43+
response: IncomingMessage;
44+
body: V1CronJobList;
45+
}> {
46+
const v1CronJobList = new V1CronJobList();
47+
v1CronJobList.apiVersion = 'batch/v1';
48+
v1CronJobList.kind = 'CronJobList';
49+
v1CronJobList.items = new Array<V1CronJob>();
50+
51+
return await paginatedClusterList(
52+
v1CronJobList,
53+
k8sApi.batchClient.listCronJobForAllNamespaces.bind(k8sApi.batchClient),
54+
);
55+
}
56+
57+
export async function paginatedNamespacedCronJobV1Beta1List(
58+
namespace: string,
59+
): Promise<{
3660
response: IncomingMessage;
3761
body: V1beta1CronJobList;
3862
}> {
@@ -41,7 +65,7 @@ export async function paginatedCronJobV1Beta1List(namespace: string): Promise<{
4165
cronJobList.kind = 'CronJobList';
4266
cronJobList.items = new Array<V1beta1CronJob>();
4367

44-
return await paginatedList(
68+
return await paginatedNamespacedList(
4569
namespace,
4670
cronJobList,
4771
k8sApi.batchUnstableClient.listNamespacedCronJob.bind(
@@ -50,6 +74,23 @@ export async function paginatedCronJobV1Beta1List(namespace: string): Promise<{
5074
);
5175
}
5276

77+
export async function paginatedClusterCronJobV1Beta1List(): Promise<{
78+
response: IncomingMessage;
79+
body: V1beta1CronJobList;
80+
}> {
81+
const cronJobList = new V1beta1CronJobList();
82+
cronJobList.apiVersion = 'batch/v1beta1';
83+
cronJobList.kind = 'CronJobList';
84+
cronJobList.items = new Array<V1beta1CronJob>();
85+
86+
return await paginatedClusterList(
87+
cronJobList,
88+
k8sApi.batchUnstableClient.listCronJobForAllNamespaces.bind(
89+
k8sApi.batchUnstableClient,
90+
),
91+
);
92+
}
93+
5394
export async function cronJobWatchHandler(
5495
cronJob: V1CronJob | V1beta1CronJob,
5596
): Promise<void> {
@@ -92,3 +133,91 @@ export async function cronJobWatchHandler(
92133
workloadName,
93134
);
94135
}
136+
137+
export async function isNamespacedCronJobSupported(
138+
workloadKind: WorkloadKind,
139+
namespace: string,
140+
client: BatchV1Api | BatchV1beta1Api,
141+
): Promise<boolean> {
142+
try {
143+
const pretty = undefined;
144+
const allowWatchBookmarks = undefined;
145+
const continueToken = undefined;
146+
const fieldSelector = undefined;
147+
const labelSelector = undefined;
148+
const limit = 1; // Try to grab only a single object
149+
const resourceVersion = undefined; // List anything in the cluster
150+
const resourceVersionMatch = undefined;
151+
const timeoutSeconds = 10; // Don't block the snyk-monitor indefinitely
152+
const attemptedApiCall = await retryKubernetesApiRequest(() =>
153+
client.listNamespacedCronJob(
154+
namespace,
155+
pretty,
156+
allowWatchBookmarks,
157+
continueToken,
158+
fieldSelector,
159+
labelSelector,
160+
limit,
161+
resourceVersion,
162+
resourceVersionMatch,
163+
timeoutSeconds,
164+
),
165+
);
166+
return (
167+
attemptedApiCall !== undefined &&
168+
attemptedApiCall.response !== undefined &&
169+
attemptedApiCall.response.statusCode !== undefined &&
170+
attemptedApiCall.response.statusCode >= 200 &&
171+
attemptedApiCall.response.statusCode < 300
172+
);
173+
} catch (error) {
174+
logger.debug(
175+
{ error, workloadKind: workloadKind },
176+
'Failed on Kubernetes API call to list CronJob or v1beta1 CronJob',
177+
);
178+
return false;
179+
}
180+
}
181+
182+
export async function isClusterCronJobSupported(
183+
workloadKind: WorkloadKind,
184+
client: BatchV1Api | BatchV1beta1Api,
185+
): Promise<boolean> {
186+
try {
187+
const pretty = undefined;
188+
const allowWatchBookmarks = undefined;
189+
const continueToken = undefined;
190+
const fieldSelector = undefined;
191+
const labelSelector = undefined;
192+
const limit = 1; // Try to grab only a single object
193+
const resourceVersion = undefined; // List anything in the cluster
194+
const resourceVersionMatch = undefined;
195+
const timeoutSeconds = 10; // Don't block the snyk-monitor indefinitely
196+
const attemptedApiCall = await retryKubernetesApiRequest(() =>
197+
client.listCronJobForAllNamespaces(
198+
allowWatchBookmarks,
199+
continueToken,
200+
fieldSelector,
201+
labelSelector,
202+
limit,
203+
pretty,
204+
resourceVersion,
205+
resourceVersionMatch,
206+
timeoutSeconds,
207+
),
208+
);
209+
return (
210+
attemptedApiCall !== undefined &&
211+
attemptedApiCall.response !== undefined &&
212+
attemptedApiCall.response.statusCode !== undefined &&
213+
attemptedApiCall.response.statusCode >= 200 &&
214+
attemptedApiCall.response.statusCode < 300
215+
);
216+
} catch (error) {
217+
logger.debug(
218+
{ error, workloadKind: workloadKind },
219+
'Failed on Kubernetes API call to list CronJob or v1beta1 CronJob',
220+
);
221+
return false;
222+
}
223+
}

src/supervisor/watchers/handlers/daemon-set.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { V1DaemonSet, V1DaemonSetList } from '@kubernetes/client-node';
2-
import { deleteWorkload, trimWorkload } from './workload';
2+
import { deleteWorkload } from './workload';
33
import { WorkloadKind } from '../../types';
44
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
55
import { IncomingMessage } from 'http';
66
import { k8sApi } from '../../cluster';
7-
import { paginatedList } from './pagination';
7+
import { paginatedClusterList, paginatedNamespacedList } from './pagination';
88
import {
99
deleteWorkloadAlreadyScanned,
1010
deleteWorkloadImagesAlreadyScanned,
1111
kubernetesObjectToWorkloadAlreadyScanned,
1212
} from '../../../state';
13+
import { trimWorkload } from '../../workload-sanitization';
1314

14-
export async function paginatedDaemonSetList(namespace: string): Promise<{
15+
export async function paginatedNamespacedDaemonSetList(
16+
namespace: string,
17+
): Promise<{
1518
response: IncomingMessage;
1619
body: V1DaemonSetList;
1720
}> {
@@ -20,13 +23,28 @@ export async function paginatedDaemonSetList(namespace: string): Promise<{
2023
v1DaemonSetList.kind = 'DaemonSetList';
2124
v1DaemonSetList.items = new Array<V1DaemonSet>();
2225

23-
return await paginatedList(
26+
return await paginatedNamespacedList(
2427
namespace,
2528
v1DaemonSetList,
2629
k8sApi.appsClient.listNamespacedDaemonSet.bind(k8sApi.appsClient),
2730
);
2831
}
2932

33+
export async function paginatedClusterDaemonSetList(): Promise<{
34+
response: IncomingMessage;
35+
body: V1DaemonSetList;
36+
}> {
37+
const v1DaemonSetList = new V1DaemonSetList();
38+
v1DaemonSetList.apiVersion = 'apps/v1';
39+
v1DaemonSetList.kind = 'DaemonSetList';
40+
v1DaemonSetList.items = new Array<V1DaemonSet>();
41+
42+
return await paginatedClusterList(
43+
v1DaemonSetList,
44+
k8sApi.appsClient.listDaemonSetForAllNamespaces.bind(k8sApi.appsClient),
45+
);
46+
}
47+
3048
export async function daemonSetWatchHandler(
3149
daemonSet: V1DaemonSet,
3250
): Promise<void> {

0 commit comments

Comments
 (0)