Skip to content

Commit a3bc137

Browse files
authored
Merge pull request #1094 from snyk/feature/argo-rollout-support
Feature/argo rollout support
2 parents 2572799 + 960ec00 commit a3bc137

File tree

20 files changed

+405
-6
lines changed

20 files changed

+405
-6
lines changed

snyk-monitor-cluster-permissions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ rules:
5454
- get
5555
- list
5656
- watch
57+
- apiGroups:
58+
- argoproj.io
59+
resources:
60+
- rollouts
61+
verbs:
62+
- get
63+
- list
64+
- watch
5765
---
5866
kind: ServiceAccount
5967
apiVersion: v1

snyk-monitor-namespaced-permissions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ rules:
4646
- get
4747
- list
4848
- watch
49+
- apiGroups:
50+
- argoproj.io
51+
resources:
52+
- rollouts
53+
verbs:
54+
- get
55+
- list
56+
- watch
4957
---
5058
kind: ServiceAccount
5159
apiVersion: v1

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

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@ export async function paginatedNamespacedDeploymentConfigList(
5151
fieldSelector,
5252
labelSelector,
5353
limit,
54-
// TODO: Why any?
54+
/**
55+
* The K8s client's listNamespacedCustomObject() doesn't allow to specify
56+
* the type of the response body and returns the generic "object" type,
57+
* but with how we declared our types we expect it to return a "KubernetesListObject" type.
58+
*
59+
* Not using "any" results in a similar error (highlighting the "body" property):
60+
* Type 'Promise<{ response: IncomingMessage; ***body: object;*** }>' is not assignable to type
61+
* 'Promise<{ response: IncomingMessage; ***body: KubernetesListObject<...>;*** }>'
62+
*/
5563
) as any,
5664
);
5765
}

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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ export interface V1DeploymentConfigStatus {
6060
observedGeneration?: number;
6161
}
6262

63+
export class V1alpha1RolloutList
64+
implements KubernetesListObject<V1alpha1Rollout>
65+
{
66+
'apiVersion'?: string;
67+
'items': Array<V1alpha1Rollout>;
68+
'kind'?: string;
69+
'metadata'?: V1ListMeta;
70+
}
71+
72+
export interface V1alpha1Rollout extends KubernetesObject {
73+
apiVersion?: string;
74+
kind?: string;
75+
metadata?: V1ObjectMeta;
76+
spec?: V1alpha1RolloutSpec;
77+
status?: V1alpha1RolloutStatus;
78+
}
79+
80+
export interface V1alpha1RolloutSpec {
81+
template: V1PodTemplateSpec;
82+
}
83+
84+
export interface V1alpha1RolloutStatus {
85+
observedGeneration?: number;
86+
}
87+
6388
export type V1ClusterList<T> = (
6489
allowWatchBookmarks?: boolean,
6590
_continue?: string,

0 commit comments

Comments
 (0)