Skip to content

Commit 5f54557

Browse files
authored
Merge pull request #472 from snyk/fix/include_errors_for_k8s_api_calls
Fix/include errors for k8s api calls
2 parents 6af0542 + 47a769f commit 5f54557

File tree

6 files changed

+117
-2
lines changed

6 files changed

+117
-2
lines changed

src/supervisor/kuberenetes-api-wrappers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ function shouldRetryRequest(err: IRequestError, attempt: number): boolean {
4646
return false;
4747
}
4848

49+
if (err.code === 'ECONNREFUSED') {
50+
return true;
51+
}
52+
53+
if (err.code === 'ETIMEDOUT') {
54+
return true;
55+
}
56+
4957
if (!(err.response)) {
5058
return false;
5159
}

src/supervisor/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum WorkloadKind {
1414
}
1515

1616
export interface IRequestError {
17+
code?: string;
1718
response?: IncomingMessage;
1819
}
1920

src/supervisor/watchers/handlers/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { replicaSetWatchHandler } from './replica-set';
1010
import { replicationControllerWatchHandler } from './replication-controller';
1111
import { statefulSetWatchHandler } from './stateful-set';
1212
import { k8sApi, kubeConfig } from '../../cluster';
13+
import * as kubernetesApiWrappers from '../../kuberenetes-api-wrappers';
1314
import { IWorkloadWatchMetadata, FALSY_WORKLOAD_NAME_MARKER } from './types';
1415

1516
/**
@@ -98,7 +99,8 @@ export function setupInformer(namespace: string, workloadKind: WorkloadKind) {
9899
const listMethod = workloadMetadata.listFactory(namespace);
99100
const loggedListMethod = async () => {
100101
try {
101-
return await listMethod();
102+
return await kubernetesApiWrappers.retryKubernetesApiRequest(
103+
() => listMethod());
102104
} catch (err) {
103105
logger.error({err, namespace, workloadKind}, 'error while listing entities on namespace');
104106
throw err;

src/supervisor/watchers/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import logger = require('../../common/logger');
66
import { WorkloadKind } from '../types';
77
import { setupInformer } from './handlers';
88
import { kubeConfig, k8sApi } from '../cluster';
9+
import * as kubernetesApiWrappers from '../kuberenetes-api-wrappers';
910
import { kubernetesInternalNamespaces } from './internal-namespaces';
1011

1112
/**
@@ -52,7 +53,8 @@ function setupWatchesForCluster(): void {
5253
'/api/v1/namespaces',
5354
async () => {
5455
try {
55-
return await k8sApi.coreClient.listNamespace();
56+
return await kubernetesApiWrappers.retryKubernetesApiRequest(
57+
() => k8sApi.coreClient.listNamespace());
5658
} catch (err) {
5759
logger.error({err}, 'error while listing namespaces');
5860
throw err;

test/system/kind.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ tap.test('Kubernetes-Monitor with KinD', async (t) => {
6969
]);
7070

7171
// Setup nocks
72+
nock(/https\:\/\/127\.0\.0\.1\:\d+/, { allowUnmocked: true})
73+
.get('/api/v1/namespaces')
74+
.times(1)
75+
.replyWithError({
76+
code: 'ECONNREFUSED'
77+
})
78+
.get('/api/v1/namespaces')
79+
.times(1)
80+
.replyWithError({
81+
code: 'ETIMEDOUT'
82+
});
83+
84+
nock(/https\:\/\/127\.0\.0\.1\:\d+/, { allowUnmocked: true})
85+
.get('/apis/apps/v1/namespaces/snyk-monitor/deployments')
86+
.times(1)
87+
.replyWithError({
88+
code: 'ECONNREFUSED'
89+
})
90+
.get('/apis/apps/v1/namespaces/snyk-monitor/deployments')
91+
.times(1)
92+
.replyWithError({
93+
code: 'ETIMEDOUT'
94+
});
95+
7296
nock('https://kubernetes-upstream.snyk.io')
7397
.post('/api/v1/workload')
7498
.times(1)

test/unit/supervisor/kubernetes-api-wrappers.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,84 @@ tap.test('calculateSleepSeconds', async (t) => {
4646
);
4747
});
4848

49+
tap.test('retryKubernetesApiRequest for ECONNREFUSED error', async (t) => {
50+
const retryableErrorResponse = {code: 'ECONNREFUSED'};
51+
52+
await t.rejects(
53+
() => kubernetesApiWrappers.retryKubernetesApiRequest(() => Promise.reject(retryableErrorResponse)),
54+
'eventually throws on repeated retryable error responses',
55+
);
56+
57+
let failures = 0;
58+
const functionThatFailsJustEnoughTimes = () => {
59+
if (failures < kubernetesApiWrappers.ATTEMPTS_MAX - 1) {
60+
failures +=1;
61+
return Promise.reject(retryableErrorResponse);
62+
}
63+
return Promise.resolve('egg');
64+
};
65+
66+
const successfulResponse = await kubernetesApiWrappers.retryKubernetesApiRequest(functionThatFailsJustEnoughTimes);
67+
t.equals(
68+
successfulResponse,
69+
'egg',
70+
'keeps retrying on ECONNREFUSED as long as we don\'t cross max attempts',
71+
);
72+
73+
failures = 0;
74+
const functionThatFailsOneTooManyTimes = () => {
75+
if (failures < kubernetesApiWrappers.ATTEMPTS_MAX) {
76+
failures +=1;
77+
return Promise.reject(retryableErrorResponse);
78+
}
79+
return Promise.resolve('egg');
80+
};
81+
82+
await t.rejects(
83+
() => kubernetesApiWrappers.retryKubernetesApiRequest(functionThatFailsOneTooManyTimes),
84+
'failure more than the maximum, rejects, even for a retryable error response',
85+
);
86+
});
87+
88+
tap.test('retryKubernetesApiRequest for ETIMEDOUT error', async (t) => {
89+
const retryableErrorResponse = {code: 'ETIMEDOUT'};
90+
91+
await t.rejects(
92+
() => kubernetesApiWrappers.retryKubernetesApiRequest(() => Promise.reject(retryableErrorResponse)),
93+
'eventually throws on repeated retryable error responses',
94+
);
95+
96+
let failures = 0;
97+
const functionThatFailsJustEnoughTimes = () => {
98+
if (failures < kubernetesApiWrappers.ATTEMPTS_MAX - 1) {
99+
failures +=1;
100+
return Promise.reject(retryableErrorResponse);
101+
}
102+
return Promise.resolve('egg');
103+
};
104+
105+
const successfulResponse = await kubernetesApiWrappers.retryKubernetesApiRequest(functionThatFailsJustEnoughTimes);
106+
t.equals(
107+
successfulResponse,
108+
'egg',
109+
'keeps retrying on ETIMEDOUT as long as we don\'t cross max attempts',
110+
);
111+
112+
failures = 0;
113+
const functionThatFailsOneTooManyTimes = () => {
114+
if (failures < kubernetesApiWrappers.ATTEMPTS_MAX) {
115+
failures +=1;
116+
return Promise.reject(retryableErrorResponse);
117+
}
118+
return Promise.resolve('egg');
119+
};
120+
121+
await t.rejects(
122+
() => kubernetesApiWrappers.retryKubernetesApiRequest(functionThatFailsOneTooManyTimes),
123+
'failure more than the maximum, rejects, even for a retryable error response',
124+
);
125+
});
126+
49127
tap.test('retryKubernetesApiRequest for retryable errors', async (t) => {
50128
const retryableErrorResponse = {response: {statusCode: 429}};
51129

0 commit comments

Comments
 (0)