Skip to content

Commit 00375d8

Browse files
steven-hadfieldivanstanev
authored andcommitted
feat: Initial support for parsing ArgoCD rollout CRD definitions
1 parent 2572799 commit 00375d8

File tree

9 files changed

+307
-1
lines changed

9 files changed

+307
-1
lines changed

snyk-monitor/templates/clusterrole.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ rules:
6161
- get
6262
- list
6363
- watch
64+
- apiGroups:
65+
- argoproj.io
66+
resources:
67+
- rollouts
68+
verbs:
69+
- get
70+
- list
71+
- watch
6472
{{- if .Values.psp.enabled }}
6573
- apiGroups:
6674
- policy

snyk-monitor/templates/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ rules:
6161
- get
6262
- list
6363
- watch
64+
- apiGroups:
65+
- argoproj.io
66+
resources:
67+
- rollouts
68+
verbs:
69+
- get
70+
- list
71+
- watch
6472
{{- if .Values.psp.enabled }}
6573
- apiGroups:
6674
- policy

src/supervisor/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum WorkloadKind {
2525
ReplicationController = 'ReplicationController',
2626
Pod = 'Pod',
2727
DeploymentConfig = 'DeploymentConfig',
28+
ArgoRollout = 'Rollout',
2829
}
2930

3031
export interface IRequestError {
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { IncomingMessage } from 'http';
2+
import { deleteWorkload } from './workload';
3+
import { WorkloadKind } from '../../types';
4+
import { FALSY_WORKLOAD_NAME_MARKER, Rollout, RolloutList } from './types';
5+
import { paginatedClusterList, paginatedNamespacedList } from './pagination';
6+
import { k8sApi } from '../../cluster';
7+
import {
8+
deleteWorkloadAlreadyScanned,
9+
deleteWorkloadImagesAlreadyScanned,
10+
kubernetesObjectToWorkloadAlreadyScanned,
11+
} from '../../../state';
12+
import { retryKubernetesApiRequest } from '../../kuberenetes-api-wrappers';
13+
import { logger } from '../../../common/logger';
14+
import { deleteWorkloadFromScanQueue } from './queue';
15+
import { trimWorkload } from '../../workload-sanitization';
16+
17+
export async function paginatedNamespacedArgoRolloutList(
18+
namespace: string,
19+
): Promise<{
20+
response: IncomingMessage;
21+
body: RolloutList;
22+
}> {
23+
const rolloutList = new RolloutList();
24+
rolloutList.apiVersion = 'argoproj.io/v1alpha1';
25+
rolloutList.kind = 'RolloutList';
26+
rolloutList.items = new Array<Rollout>();
27+
28+
return await paginatedNamespacedList(
29+
namespace,
30+
rolloutList,
31+
async (
32+
namespace: string,
33+
pretty?: string,
34+
_allowWatchBookmarks?: boolean,
35+
_continue?: string,
36+
fieldSelector?: string,
37+
labelSelector?: string,
38+
limit?: number,
39+
) =>
40+
k8sApi.customObjectsClient.listNamespacedCustomObject(
41+
'argoproj.io',
42+
'v1alpha1',
43+
namespace,
44+
'rollouts',
45+
pretty,
46+
_continue,
47+
fieldSelector,
48+
labelSelector,
49+
limit,
50+
// TODO: Why any?
51+
) as any,
52+
);
53+
}
54+
55+
export async function paginatedClusterArgoRolloutList(): Promise<{
56+
response: IncomingMessage;
57+
body: RolloutList;
58+
}> {
59+
const rolloutList = new RolloutList();
60+
rolloutList.apiVersion = 'argoproj.io/v1';
61+
rolloutList.kind = 'RolloutList';
62+
rolloutList.items = new Array<Rollout>();
63+
64+
return await paginatedClusterList(
65+
rolloutList,
66+
async (
67+
_allowWatchBookmarks?: boolean,
68+
_continue?: string,
69+
fieldSelector?: string,
70+
labelSelector?: string,
71+
limit?: number,
72+
pretty?: string,
73+
) =>
74+
k8sApi.customObjectsClient.listClusterCustomObject(
75+
'argoproj.io',
76+
'v1alpha1',
77+
'rollouts',
78+
pretty,
79+
_continue,
80+
fieldSelector,
81+
labelSelector,
82+
limit,
83+
) as any,
84+
);
85+
}
86+
87+
export async function ArgoRolloutWatchHandler(rollout: Rollout): Promise<void> {
88+
rollout = trimWorkload(rollout);
89+
90+
if (
91+
!rollout.metadata ||
92+
!rollout.spec ||
93+
!rollout.spec.template.metadata ||
94+
!rollout.spec.template.spec ||
95+
!rollout.status
96+
) {
97+
return;
98+
}
99+
100+
const workloadAlreadyScanned =
101+
kubernetesObjectToWorkloadAlreadyScanned(rollout);
102+
if (workloadAlreadyScanned !== undefined) {
103+
await Promise.all([
104+
deleteWorkloadAlreadyScanned(workloadAlreadyScanned),
105+
deleteWorkloadImagesAlreadyScanned({
106+
...workloadAlreadyScanned,
107+
imageIds: rollout.spec.template.spec.containers
108+
.filter((container) => container.image !== undefined)
109+
.map((container) => container.image!),
110+
}),
111+
deleteWorkloadFromScanQueue(workloadAlreadyScanned),
112+
]);
113+
}
114+
115+
const workloadName = rollout.metadata.name || FALSY_WORKLOAD_NAME_MARKER;
116+
117+
await deleteWorkload(
118+
{
119+
kind: WorkloadKind.ArgoRollout,
120+
objectMeta: rollout.metadata,
121+
specMeta: rollout.spec.template.metadata,
122+
ownerRefs: rollout.metadata.ownerReferences,
123+
revision: rollout.status.observedGeneration,
124+
podSpec: rollout.spec.template.spec,
125+
},
126+
workloadName,
127+
);
128+
}
129+
130+
export async function isNamespacedArgoRolloutSupported(
131+
namespace: string,
132+
): Promise<boolean> {
133+
try {
134+
const pretty = undefined;
135+
const continueToken = undefined;
136+
const fieldSelector = undefined;
137+
const labelSelector = undefined;
138+
const limit = 1; // Try to grab only a single object
139+
const resourceVersion = undefined; // List anything in the cluster
140+
const timeoutSeconds = 10; // Don't block the snyk-monitor indefinitely
141+
const attemptedApiCall = await retryKubernetesApiRequest(() =>
142+
k8sApi.customObjectsClient.listNamespacedCustomObject(
143+
'argoproj.io',
144+
'v1alpha1',
145+
namespace,
146+
'rollouts',
147+
pretty,
148+
continueToken,
149+
fieldSelector,
150+
labelSelector,
151+
limit,
152+
resourceVersion,
153+
timeoutSeconds,
154+
),
155+
);
156+
return (
157+
attemptedApiCall !== undefined &&
158+
attemptedApiCall.response !== undefined &&
159+
attemptedApiCall.response.statusCode !== undefined &&
160+
attemptedApiCall.response.statusCode >= 200 &&
161+
attemptedApiCall.response.statusCode < 300
162+
);
163+
} catch (error) {
164+
logger.debug(
165+
{ error, workloadKind: WorkloadKind.ArgoRollout },
166+
'Failed on Kubernetes API call to list namespaced argoproj.io/Rollout',
167+
);
168+
return false;
169+
}
170+
}
171+
172+
export async function isClusterArgoRolloutSupported(): Promise<boolean> {
173+
try {
174+
const pretty = undefined;
175+
const continueToken = undefined;
176+
const fieldSelector = undefined;
177+
const labelSelector = undefined;
178+
const limit = 1; // Try to grab only a single object
179+
const resourceVersion = undefined; // List anything in the cluster
180+
const timeoutSeconds = 10; // Don't block the snyk-monitor indefinitely
181+
const attemptedApiCall = await retryKubernetesApiRequest(() =>
182+
k8sApi.customObjectsClient.listClusterCustomObject(
183+
'argoproj.io',
184+
'v1alpha1',
185+
'rollouts',
186+
pretty,
187+
continueToken,
188+
fieldSelector,
189+
labelSelector,
190+
limit,
191+
resourceVersion,
192+
timeoutSeconds,
193+
),
194+
);
195+
return (
196+
attemptedApiCall !== undefined &&
197+
attemptedApiCall.response !== undefined &&
198+
attemptedApiCall.response.statusCode !== undefined &&
199+
attemptedApiCall.response.statusCode >= 200 &&
200+
attemptedApiCall.response.statusCode < 300
201+
);
202+
} catch (error) {
203+
logger.debug(
204+
{ error, workloadKind: WorkloadKind.ArgoRollout },
205+
'Failed on Kubernetes API call to list cluster argoproj.io/Rollout',
206+
);
207+
return false;
208+
}
209+
}

src/supervisor/watchers/handlers/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { logger } from '../../../common/logger';
44
import { WorkloadKind } from '../../types';
55
import * as cronJob from './cron-job';
66
import * as deploymentConfig from './deployment-config';
7+
import * as rollout from './argo-rollout';
78
import { k8sApi, kubeConfig } from '../../cluster';
89
import * as kubernetesApiWrappers from '../../kuberenetes-api-wrappers';
910
import { FALSY_WORKLOAD_NAME_MARKER } from './types';
@@ -16,6 +17,8 @@ async function isSupportedNamespacedWorkload(
1617
workloadKind: WorkloadKind,
1718
): Promise<boolean> {
1819
switch (workloadKind) {
20+
case WorkloadKind.ArgoRollout:
21+
return await rollout.isNamespacedArgoRolloutSupported(namespace);
1922
case WorkloadKind.DeploymentConfig:
2023
return await deploymentConfig.isNamespacedDeploymentConfigSupported(
2124
namespace,
@@ -43,6 +46,8 @@ async function isSupportedClusterWorkload(
4346
switch (workloadKind) {
4447
case WorkloadKind.DeploymentConfig:
4548
return await deploymentConfig.isClusterDeploymentConfigSupported();
49+
case WorkloadKind.ArgoRollout:
50+
return await rollout.isClusterArgoRolloutSupported();
4651
case WorkloadKind.CronJobV1Beta1:
4752
return await cronJob.isClusterCronJobSupported(
4853
workloadKind,

src/supervisor/watchers/handlers/informer-config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as replicaSet from './replica-set';
1010
import * as replicationController from './replication-controller';
1111
import * as statefulSet from './stateful-set';
1212
import * as deploymentConfig from './deployment-config';
13+
import * as rollout from './argo-rollout';
1314
import { IWorkloadWatchMetadata } from './types';
1415

1516
/**
@@ -146,4 +147,15 @@ export const workloadWatchMetadata: Readonly<IWorkloadWatchMetadata> = {
146147
namespacedListFactory: (namespace) => () =>
147148
deploymentConfig.paginatedNamespacedDeploymentConfigList(namespace),
148149
},
150+
[WorkloadKind.ArgoRollout]: {
151+
clusterEndpoint: '/apis/argoproj.io/v1alpha1/rollouts',
152+
namespacedEndpoint:
153+
'/apis/argoproj.io/v1alpha1/watch/namespaces/{namespace}/rollouts',
154+
handlers: {
155+
[DELETE]: rollout.ArgoRolloutWatchHandler,
156+
},
157+
clusterListFactory: () => () => rollout.paginatedClusterArgoRolloutList(),
158+
namespacedListFactory: (namespace) => () =>
159+
rollout.paginatedNamespacedArgoRolloutList(namespace),
160+
},
149161
};

src/supervisor/watchers/handlers/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,29 @@ export interface V1DeploymentConfigStatus {
6060
observedGeneration?: number;
6161
}
6262

63+
export class RolloutList implements KubernetesListObject<Rollout> {
64+
'apiVersion'?: string;
65+
'items': Array<Rollout>;
66+
'kind'?: string;
67+
'metadata'?: V1ListMeta;
68+
}
69+
70+
export interface Rollout extends KubernetesObject {
71+
apiVersion?: string;
72+
kind?: string;
73+
metadata?: V1ObjectMeta;
74+
spec?: RolloutSpec;
75+
status?: RolloutStatus;
76+
}
77+
78+
export interface RolloutSpec {
79+
template: V1PodTemplateSpec;
80+
}
81+
82+
export interface RolloutStatus {
83+
observedGeneration?: number;
84+
}
85+
6386
export type V1ClusterList<T> = (
6487
allowWatchBookmarks?: boolean,
6588
_continue?: string,

src/supervisor/workload-reader.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as kubernetesApiWrappers from './kuberenetes-api-wrappers';
44
import { k8sApi } from './cluster';
55
import { IKubeObjectMetadata, WorkloadKind } from './types';
66
import { logger } from '../common/logger';
7-
import { V1DeploymentConfig } from './watchers/handlers/types';
7+
import { Rollout, V1DeploymentConfig } from './watchers/handlers/types';
88
import { trimWorkload } from './workload-sanitization';
99

1010
type IKubeObjectMetadataWithoutPodSpec = Omit<IKubeObjectMetadata, 'podSpec'>;
@@ -280,6 +280,44 @@ const replicationControllerReader: IWorkloadReaderFunc = async (
280280
return metadata;
281281
};
282282

283+
const argoRolloutReader: IWorkloadReaderFunc = async (
284+
workloadName,
285+
namespace,
286+
) => {
287+
const rolloutResult = await kubernetesApiWrappers.retryKubernetesApiRequest(
288+
() =>
289+
k8sApi.customObjectsClient.getNamespacedCustomObject(
290+
'argoproj.io',
291+
'v1alpha1',
292+
namespace,
293+
'rollouts',
294+
workloadName,
295+
),
296+
);
297+
const rollout: Rollout = trimWorkload(rolloutResult.body);
298+
299+
if (
300+
!rollout.metadata ||
301+
!rollout.spec ||
302+
!rollout.spec.template.metadata ||
303+
!rollout.spec.template.spec ||
304+
!rollout.status
305+
) {
306+
logIncompleteWorkload(workloadName, namespace);
307+
308+
return undefined;
309+
}
310+
311+
const metadata: IKubeObjectMetadataWithoutPodSpec = {
312+
kind: WorkloadKind.ArgoRollout,
313+
objectMeta: rollout.metadata,
314+
specMeta: rollout.spec.template.metadata,
315+
ownerRefs: rollout.metadata.ownerReferences,
316+
revision: rollout.status.observedGeneration,
317+
};
318+
return metadata;
319+
};
320+
283321
function logIncompleteWorkload(workloadName: string, namespace: string): void {
284322
logger.info(
285323
{ workloadName, namespace },
@@ -292,6 +330,7 @@ function logIncompleteWorkload(workloadName: string, namespace: string): void {
292330
// and just grab a generic handler/reader that does that for us (based on the "kind").
293331
const workloadReader: Record<string, IWorkloadReaderFunc> = {
294332
[WorkloadKind.Deployment]: deploymentReader,
333+
[WorkloadKind.ArgoRollout]: argoRolloutReader,
295334
[WorkloadKind.ReplicaSet]: replicaSetReader,
296335
[WorkloadKind.StatefulSet]: statefulSetReader,
297336
[WorkloadKind.DaemonSet]: daemonSetReader,

src/transmitter/payload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ const workloadKindMap = {
150150
replicationcontroller: 'ReplicationController',
151151
deploymentconfig: 'DeploymentConfig',
152152
pod: 'Pod',
153+
rollout: 'Rollout', // TODO: Verify if supported
153154
};
154155
export function constructRuntimeData(
155156
runtimeResults: IRuntimeImage[],

0 commit comments

Comments
 (0)