Skip to content

Commit da87ff0

Browse files
author
Tal Kaptsan
authored
Merge pull request #144 from snyk/feat/workload-metadata
Feat/send workload metadata
2 parents dc7a447 + dd79178 commit da87ff0

File tree

7 files changed

+134
-19
lines changed

7 files changed

+134
-19
lines changed

src/kube-scanner/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import logger = require('../common/logger');
99
import { pullImages } from '../images';
1010
import { scanImages, IScanResult } from './image-scanner';
1111
import { deleteHomebaseWorkload, sendDepGraph } from '../transmitter';
12-
import { constructHomebaseDeleteWorkloadPayload, constructHomebaseWorkloadPayloads } from '../transmitter/payload';
12+
import { constructHomebaseDeleteWorkloadPayload, constructHomebaseDepGraphPayloads } from '../transmitter/payload';
1313
import { IDepGraphPayload, IWorkload, ILocalWorkloadLocator } from '../transmitter/types';
1414

1515
export = class WorkloadWorker {
@@ -40,8 +40,8 @@ export = class WorkloadWorker {
4040
return;
4141
}
4242

43-
const homebasePayloads: IDepGraphPayload[] = constructHomebaseWorkloadPayloads(scannedImages, workloadMetadata);
44-
await sendDepGraph(...homebasePayloads);
43+
const depGraphPayloads: IDepGraphPayload[] = constructHomebaseDepGraphPayloads(scannedImages, workloadMetadata);
44+
await sendDepGraph(...depGraphPayloads);
4545

4646
const pulledImageMetadata = workloadMetadata.filter((meta) =>
4747
pulledImages.includes(meta.imageName));

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import async = require('async');
33
import config = require('../../../common/config');
44
import logger = require('../../../common/logger');
55
import WorkloadWorker = require('../../../kube-scanner');
6+
import { sendWorkloadMetadata } from '../../../transmitter';
67
import { IWorkload } from '../../../transmitter/types';
8+
import { constructHomebaseWorkloadMetadataPayload } from '../../../transmitter/payload';
79
import { buildMetadataForWorkload } from '../../metadata-extractor';
810
import { PodPhase } from '../types';
911
import state = require('../../../state');
@@ -59,7 +61,11 @@ export async function podWatchHandler(pod: V1Pod) {
5961
return;
6062
}
6163

62-
const workloadName = workloadMetadata[0].name;
64+
// every element contains the workload information, so we can get it from the first one
65+
const workloadMember = workloadMetadata[0];
66+
const workloadMetadataPayload = constructHomebaseWorkloadMetadataPayload(workloadMember);
67+
sendWorkloadMetadata(workloadMetadataPayload);
68+
const workloadName = workloadMember.name;
6369
const workloadWorker = new WorkloadWorker(workloadName);
6470
await handleReadyPod(workloadWorker, workloadMetadata);
6571
} catch (error) {

src/transmitter/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import needle = require('needle');
22
import * as config from '../common/config';
33
import logger = require('../common/logger');
4-
import { IDeleteWorkloadPayload, IDepGraphPayload } from './types';
4+
import { IDeleteWorkloadPayload, IDepGraphPayload, IWorkloadMetadataPayload } from './types';
55

66
const homebaseUrl = config.INTEGRATION_API || config.DEFAULT_HOMEBASE_URL;
77

@@ -27,6 +27,22 @@ export async function sendDepGraph(...payloads: IDepGraphPayload[]) {
2727
}
2828
}
2929

30+
export async function sendWorkloadMetadata(payload: IWorkloadMetadataPayload) {
31+
try {
32+
const result = await needle('post', `${homebaseUrl}/api/v1/workload`, payload, {
33+
json: true,
34+
compressed: true,
35+
},
36+
);
37+
38+
if (!isSuccessStatusCode(result.statusCode)) {
39+
throw new Error(`${result.statusCode} ${result.statusMessage}`);
40+
}
41+
} catch (error) {
42+
logger.error({error}, 'Could not send workload metadata to Homebase');
43+
}
44+
}
45+
3046
export async function deleteHomebaseWorkload(payload: IDeleteWorkloadPayload) {
3147
try {
3248
const result = await needle('delete', `${homebaseUrl}/api/v1/workload`, payload, {

src/transmitter/payload.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import config = require('../common/config');
22
import { currentClusterName } from '../kube-scanner/cluster';
33
import { IScanResult } from '../kube-scanner/image-scanner';
4-
import { IDeleteWorkloadPayload, IDepGraphPayload, IWorkload, ILocalWorkloadLocator, IImageLocator } from './types';
4+
import {
5+
IDeleteWorkloadPayload,
6+
IDepGraphPayload,
7+
IWorkload,
8+
ILocalWorkloadLocator,
9+
IImageLocator,
10+
IWorkloadMetadataPayload,
11+
IWorkloadMetadata,
12+
IWorkloadLocator,
13+
} from './types';
514

6-
export function constructHomebaseWorkloadPayloads(
15+
export function constructHomebaseDepGraphPayloads(
716
scannedImages: IScanResult[],
817
workloadMetadata: IWorkload[],
918
): IDepGraphPayload[] {
@@ -31,6 +40,28 @@ export function constructHomebaseWorkloadPayloads(
3140
return results;
3241
}
3342

43+
export function constructHomebaseWorkloadMetadataPayload(workload: IWorkload): IWorkloadMetadataPayload {
44+
if (!workload) {
45+
throw new Error('can\'t build workload metadata payload for undefined workload');
46+
}
47+
48+
const workloadLocator: IWorkloadLocator = {
49+
userLocator: config.INTEGRATION_ID,
50+
cluster: workload.cluster,
51+
namespace: workload.namespace,
52+
type: workload.type,
53+
name: workload.name,
54+
};
55+
const workloadMetadata: IWorkloadMetadata = {
56+
labels: workload.labels,
57+
specLabels: workload.specLabels,
58+
annotations: workload.annotations,
59+
specAnnotations: workload.specAnnotations,
60+
revision: workload.revision,
61+
};
62+
return { workloadLocator, agentId: config.AGENT_ID, workloadMetadata };
63+
}
64+
3465
export function constructHomebaseDeleteWorkloadPayload(
3566
localWorkloadLocator: ILocalWorkloadLocator,
3667
): IDeleteWorkloadPayload {

src/transmitter/types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ export interface IWorkloadLocator extends ILocalWorkloadLocator {
1111
cluster: string;
1212
}
1313

14-
export interface IWorkloadMetadata extends IWorkloadLocator {
14+
export interface IWorkloadMetadata {
1515
labels: StringMap | undefined;
16+
specLabels: StringMap | undefined;
1617
annotations: StringMap | undefined;
17-
uid: string;
18+
specAnnotations: StringMap | undefined;
19+
revision: number | undefined;
1820
}
1921

2022
export interface IImageLocator extends IWorkloadLocator {
@@ -27,6 +29,12 @@ export interface IDepGraphPayload {
2729
dependencyGraph?: any;
2830
}
2931

32+
export interface IWorkloadMetadataPayload {
33+
workloadLocator: IWorkloadLocator;
34+
agentId: string;
35+
workloadMetadata: IWorkloadMetadata;
36+
}
37+
3038
export interface IDeleteWorkloadPayload {
3139
workloadLocator: IWorkloadLocator;
3240
agentId: string;

test/integration/kubernetes.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import setup = require('../setup'); // Must be located before 'tap' import
55
// tslint:disable-next-line: ordered-imports
66
import * as tap from 'tap';
77
import * as config from '../../src/common/config';
8-
import { IWorkloadLocator } from '../../src/transmitter/types';
8+
import { IWorkloadLocator, IWorkloadMetadata } from '../../src/transmitter/types';
99
import { getKindConfigPath } from '../helpers/kind';
1010
import { WorkloadKind } from '../../src/kube-scanner/types';
1111

@@ -14,6 +14,7 @@ const toneDownFactor = 5;
1414
const maxPodChecks = setup.KUBERNETES_MONITOR_MAX_WAIT_TIME_SECONDS / toneDownFactor;
1515

1616
type WorkloadLocatorValidator = (workloads: IWorkloadLocator[] | undefined) => boolean;
17+
type WorkloadMetadataValidator = (workloadInfo: IWorkloadMetadata | undefined) => boolean;
1718

1819
async function tearDown() {
1920
console.log('Begin removing the snyk-monitor...');
@@ -75,7 +76,6 @@ tap.test('snyk-monitor container started', async (t) => {
7576
async function validateHomebaseStoredData(
7677
validatorFn: WorkloadLocatorValidator, relativeUrl: string, remainingChecks: number = maxPodChecks,
7778
): Promise<boolean> {
78-
// TODO: consider if we're OK to expose this publicly?
7979
while (remainingChecks > 0) {
8080
const responseBody = await getHomebaseResponseBody(relativeUrl);
8181
const workloads: IWorkloadLocator[] | undefined = responseBody.workloads;
@@ -89,6 +89,22 @@ async function validateHomebaseStoredData(
8989
return false;
9090
}
9191

92+
async function validateHomebaseStoredMetadata(
93+
validatorFn: WorkloadMetadataValidator, relativeUrl: string, remainingChecks: number = maxPodChecks,
94+
): Promise<boolean> {
95+
while (remainingChecks > 0) {
96+
const responseBody = await getHomebaseResponseBody(relativeUrl);
97+
const workloadInfo: IWorkloadMetadata | undefined = responseBody.workloadInfo;
98+
const result = validatorFn(workloadInfo);
99+
if (result) {
100+
return true;
101+
}
102+
await sleep(1000 * toneDownFactor);
103+
remainingChecks--;
104+
}
105+
return false;
106+
}
107+
92108
async function getHomebaseResponseBody(relativeUrl: string): Promise<any> {
93109
const url = `https://${config.INTERNAL_PROXY_CREDENTIALS}@homebase-int.dev.snyk.io/${relativeUrl}`;
94110
const homebaseResponse = await needle('get', url, null);
@@ -97,7 +113,7 @@ async function getHomebaseResponseBody(relativeUrl: string): Promise<any> {
97113
}
98114

99115
tap.test('snyk-monitor sends data to homebase', async (t) => {
100-
t.plan(1);
116+
t.plan(2);
101117

102118
console.log(`Begin polling Homebase for the expected workloads with integration ID ${integrationId}...`);
103119

@@ -113,10 +129,18 @@ tap.test('snyk-monitor sends data to homebase', async (t) => {
113129
workload.type === WorkloadKind.Deployment) !== undefined;
114130
};
115131

132+
const metaValidator: WorkloadMetadataValidator = (workloadInfo) => {
133+
return workloadInfo !== undefined && 'revision' in workloadInfo && 'labels' in workloadInfo &&
134+
'specLabels' in workloadInfo && 'annotations' in workloadInfo && 'specAnnotations' in workloadInfo;
135+
};
136+
116137
// We don't want to spam Homebase with requests; do it infrequently
117-
const homebaseTestResult = await validateHomebaseStoredData(
138+
const homebaseDepGraphTestResult = await validateHomebaseStoredData(
118139
validatorFn, `api/v2/workloads/${integrationId}/Default cluster/services`);
119-
t.ok(homebaseTestResult, 'snyk-monitor sent expected data to homebase in the expected timeframe');
140+
t.ok(homebaseDepGraphTestResult, 'snyk-monitor sent expected data to homebase in the expected timeframe');
141+
const homebaseWorkloadMetadataResult = await validateHomebaseStoredMetadata(metaValidator,
142+
`api/v1/workload/${integrationId}/Default cluster/services/Deployment/redis`);
143+
t.ok(homebaseWorkloadMetadataResult, 'snyk-monitor sent expected metadata in the expected timeframe');
120144
});
121145

122146
tap.test('snyk-monitor sends correct data to homebase after adding another deployment', async (t) => {

test/unit/transmitter-payload.test.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import imageScanner = require('../../src/kube-scanner/image-scanner');
44
import payload = require('../../src/transmitter/payload');
55
import transmitterTypes = require('../../src/transmitter/types');
66

7-
tap.test('constructHomebaseWorkloadPayloads breaks when workloadMetadata is missing items', async (t) => {
7+
tap.test('constructHomebaseDepGraphPayloads breaks when workloadMetadata is missing items', async (t) => {
88
const scannedImages: imageScanner.IScanResult[] = [
99
{
1010
image: 'myImage',
@@ -36,11 +36,11 @@ tap.test('constructHomebaseWorkloadPayloads breaks when workloadMetadata is miss
3636
},
3737
];
3838

39-
t.throws(() => payload.constructHomebaseWorkloadPayloads(scannedImages, workloadMetadata),
40-
'constructHomebaseWorkloadPayloads throws when workloadMetadata is missing items from scannedImages');
39+
t.throws(() => payload.constructHomebaseDepGraphPayloads(scannedImages, workloadMetadata),
40+
'constructHomebaseDepGraphPayloads throws when workloadMetadata is missing items from scannedImages');
4141
});
4242

43-
tap.test('constructHomebaseWorkloadPayloads happy flow', async (t) => {
43+
tap.test('constructHomebaseDepGraphPayloads happy flow', async (t) => {
4444
const scannedImages: imageScanner.IScanResult[] = [
4545
{
4646
image: 'myImage',
@@ -67,7 +67,7 @@ tap.test('constructHomebaseWorkloadPayloads happy flow', async (t) => {
6767
},
6868
];
6969

70-
const payloads = payload.constructHomebaseWorkloadPayloads(scannedImages, workloadMetadata);
70+
const payloads = payload.constructHomebaseDepGraphPayloads(scannedImages, workloadMetadata);
7171

7272
t.equals(payloads.length, 1, 'one payload to send to Homebase');
7373
t.equals(payloads[0].dependencyGraph, JSON.stringify('whatever1'), 'dependency graph present in payload');
@@ -76,3 +76,33 @@ tap.test('constructHomebaseWorkloadPayloads happy flow', async (t) => {
7676
t.equals(payloads[0].imageLocator.name, 'workloadName', 'workload name present in payload');
7777
t.equals(payloads[0].imageLocator.type, 'type', 'workload type present in payload');
7878
});
79+
80+
tap.test('constructHomebaseWorkloadMetadataPayload happy flow', async (t) => {
81+
const workloadWithImages: transmitterTypes.IWorkload = {
82+
type: 'type',
83+
name: 'workloadName',
84+
namespace: 'spacename',
85+
labels: undefined,
86+
annotations: undefined,
87+
uid: 'udi',
88+
specLabels: undefined,
89+
specAnnotations: undefined,
90+
containerName: 'contener',
91+
imageName: 'myImage:tag',
92+
imageId: 'does this matter?',
93+
cluster: 'grapefruit',
94+
revision: 1,
95+
};
96+
97+
const workloadMetadataPayload = payload.constructHomebaseWorkloadMetadataPayload(workloadWithImages);
98+
99+
t.equals(workloadMetadataPayload.workloadLocator.cluster, 'grapefruit', 'cluster present in payload');
100+
t.equals(workloadMetadataPayload.workloadLocator.namespace, 'spacename', 'image ID present in payload');
101+
t.equals(workloadMetadataPayload.workloadLocator.name, 'workloadName', 'workload name present in payload');
102+
t.equals(workloadMetadataPayload.workloadLocator.type, 'type', 'workload type present in payload');
103+
t.equals(workloadMetadataPayload.workloadMetadata.revision, 1, 'revision present in metadata');
104+
t.ok('annotations' in workloadMetadataPayload.workloadMetadata, 'annotations present in metadata');
105+
t.ok('specAnnotations' in workloadMetadataPayload.workloadMetadata, 'specAnnotations present in metadata');
106+
t.ok('labels' in workloadMetadataPayload.workloadMetadata, 'labels present in metadata');
107+
t.ok('specLabels' in workloadMetadataPayload.workloadMetadata, 'specLabels present in metadata');
108+
});

0 commit comments

Comments
 (0)