Skip to content

Commit ab91fb8

Browse files
author
Amir Moualem
authored
Merge pull request #268 from snyk/feat/pulls-from-ecr
Feat/pulls from ecr
2 parents 7960e46 + a22c7b1 commit ab91fb8

File tree

9 files changed

+293
-17
lines changed

9 files changed

+293
-17
lines changed

package-lock.json

+89-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@types/node": "^10.12.5",
3434
"@types/sinon": "^7.5.1",
3535
"async": "^2.6.2",
36+
"aws-sdk": "^2.596.0",
3637
"bunyan": "^1.8.12",
3738
"child-process-promise": "^2.2.1",
3839
"lru-cache": "^5.1.1",

src/common/process.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { spawn, SpawnPromiseResult } from 'child-process-promise';
22
import logger = require('./logger');
33

4-
export function exec(bin: string, ...args: string[]):
4+
export interface IProcessArgument {
5+
body: string;
6+
sanitise: boolean;
7+
}
8+
9+
export function exec(bin: string, ...processArgs: IProcessArgument[]):
510
Promise<SpawnPromiseResult> {
611
if (process.env.DEBUG === 'true') {
7-
args.push('--debug');
12+
processArgs.push({body: '--debug', sanitise: false});
813
}
914

1015
// Ensure we're not passing the whole environment to the shelled out process...
@@ -13,10 +18,12 @@ export function exec(bin: string, ...args: string[]):
1318
PATH: process.env.PATH,
1419
};
1520

16-
return spawn(bin, args, { env, capture: [ 'stdout', 'stderr' ] })
21+
const allArguments = processArgs.map((arg) => arg.body);
22+
return spawn(bin, allArguments, { env, capture: [ 'stdout', 'stderr' ] })
1723
.catch((error) => {
1824
const message = (error && error.stderr) || 'Unknown reason';
19-
logger.warn({message, bin, args}, 'could not spawn the process');
25+
const loggableArguments = processArgs.filter((arg) => !arg.sanitise).map((arg) => arg.body);
26+
logger.warn({message, bin, loggableArguments}, 'child process failure');
2027
throw error;
2128
});
2229
}

src/images/credentials.ts

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as aws from 'aws-sdk';
2+
3+
import logger = require('../common/logger');
4+
5+
export async function getSourceCredentials(imageSource: string): Promise<string | undefined> {
6+
// TODO is this the best way we can determine the image's source?
7+
if (imageSource.indexOf('.ecr.') !== -1) {
8+
const ecrRegion = ecrRegionFromFullImageName(imageSource);
9+
return getEcrCredentials(ecrRegion);
10+
}
11+
return undefined;
12+
}
13+
14+
function getEcrCredentials(region: string): Promise<string> {
15+
return new Promise(async (resolve, reject) => {
16+
const ecr = new aws.ECR({region});
17+
return ecr.getAuthorizationToken({}, (err, data) => {
18+
if (err) {
19+
return reject(err);
20+
}
21+
22+
if (!(
23+
data &&
24+
data.authorizationData &&
25+
Array.isArray(data.authorizationData) &&
26+
data.authorizationData.length > 0
27+
)) {
28+
return reject('unexpected data format from ecr.getAuthorizationToken');
29+
}
30+
31+
const authorizationTokenBase64 = data.authorizationData[0].authorizationToken;
32+
33+
if (!authorizationTokenBase64) {
34+
return reject('empty authorization token from ecr.getAuthorizationToken');
35+
}
36+
37+
const buff = new Buffer(authorizationTokenBase64, 'base64');
38+
const userColonPassword = buff.toString('utf-8');
39+
return resolve(userColonPassword);
40+
});
41+
});
42+
}
43+
44+
export function ecrRegionFromFullImageName(imageFullName: string): string {
45+
// should look like this
46+
// aws_account_id.dkr.ecr.region.amazonaws.com/my-web-app:latest
47+
// https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_on_EKS.html
48+
try {
49+
const [registry, repository] = imageFullName.split('/');
50+
if (!repository) {
51+
throw new Error('ECR image full name missing repository');
52+
}
53+
54+
const parts = registry.split('.');
55+
if (!(
56+
parts.length === 6 &&
57+
parts[1] === 'dkr' &&
58+
parts[2] === 'ecr' &&
59+
parts[4] === 'amazonaws'
60+
)) {
61+
throw new Error('ECR image full name in unexpected format');
62+
}
63+
return parts[3];
64+
} catch (err) {
65+
logger.error({err, imageFullName}, 'failed extracting ECR region from image full name');
66+
throw err;
67+
}
68+
}

src/images/skopeo.ts

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { SkopeoRepositoryType } from '../kube-scanner/types';
21
import { SpawnPromiseResult } from 'child-process-promise';
3-
import { exec } from '../common/process';
4-
import config = require('../common/config');
2+
3+
import * as processWrapper from '../common/process';
4+
import * as config from'../common/config';
5+
import * as credentials from './credentials';
6+
import { SkopeoRepositoryType } from '../kube-scanner/types';
57

68
function getUniqueIdentifier(): string {
79
const [seconds, nanoseconds] = process.hrtime();
@@ -28,12 +30,27 @@ function prefixRespository(target: string, type: SkopeoRepositoryType): string {
2830
}
2931
}
3032

31-
export function pull(
33+
export async function pull(
3234
image: string,
3335
destination: string,
3436
): Promise<SpawnPromiseResult> {
35-
return exec('skopeo', 'copy',
36-
prefixRespository(image, SkopeoRepositoryType.ImageRegistry),
37-
prefixRespository(destination, SkopeoRepositoryType.DockerArchive),
38-
);
37+
const creds = await credentials.getSourceCredentials(image);
38+
const credentialsParameters = getCredentialParameters(creds);
39+
40+
const args: Array<processWrapper.IProcessArgument> = [];
41+
args.push({body: 'copy', sanitise: false});
42+
args.push(...credentialsParameters);
43+
args.push({body: prefixRespository(image, SkopeoRepositoryType.ImageRegistry), sanitise: false});
44+
args.push({body: prefixRespository(destination, SkopeoRepositoryType.DockerArchive), sanitise: false});
45+
46+
return processWrapper.exec('skopeo', ...args);
47+
}
48+
49+
export function getCredentialParameters(credentials: string | undefined): Array<processWrapper.IProcessArgument> {
50+
const credentialsParameters: Array<processWrapper.IProcessArgument> = [];
51+
if (credentials) {
52+
credentialsParameters.push({body: '--src-creds', sanitise: true});
53+
credentialsParameters.push({body: credentials, sanitise: true});
54+
}
55+
return credentialsParameters;
3956
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: debian-ecr
5+
namespace: services
6+
spec:
7+
selector:
8+
matchLabels:
9+
app: debian-ecr
10+
template:
11+
metadata:
12+
labels:
13+
app: debian-ecr
14+
spec:
15+
containers:
16+
- name: debian-ecr
17+
image: 291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/debian:10
18+
imagePullPolicy: Always
19+
securityContext: {}
20+
command: ['sh', '-c', 'echo Hello from ECR alpine pod! && sleep 360000']

test/integration/kubernetes.test.ts

+37-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ tap.test('snyk-monitor sends correct data to homebase after adding another deplo
134134
'snyk-monitor sent expected data to homebase in the expected timeframe');
135135
});
136136

137-
tap.test('snyk-monitor pulls images from private registries and sends data to homebase', async (t) => {
137+
tap.test('snyk-monitor pulls images from a private gcr.io registry and sends data to homebase', async (t) => {
138138
t.plan(3);
139139

140140
const deploymentName = 'debian-gcr-io';
@@ -144,7 +144,42 @@ tap.test('snyk-monitor pulls images from private registries and sends data to ho
144144
const imageName = 'gcr.io/snyk-k8s-fixtures/debian';
145145

146146
await kubectl.applyK8sYaml('./test/fixtures/private-registries/debian-deployment-gcr-io.yaml');
147-
console.log(`Begin polling upstream for the expected private registry workload with integration ID ${integrationId}...`);
147+
console.log(`Begin polling upstream for the expected private gcr.io image with integration ID ${integrationId}...`);
148+
149+
const validatorFn: WorkloadLocatorValidator = (workloads) => {
150+
return workloads !== undefined &&
151+
workloads.find((workload) => workload.name === deploymentName &&
152+
workload.type === WorkloadKind.Deployment) !== undefined;
153+
};
154+
155+
const homebaseTestResult = await validateHomebaseStoredData(
156+
validatorFn, `api/v2/workloads/${integrationId}/${clusterName}/${namespace}`);
157+
t.ok(homebaseTestResult, 'snyk-monitor sent expected data to upstream in the expected timeframe');
158+
159+
const depGraphResult = await getHomebaseResponseBody(
160+
`api/v1/dependency-graphs/${integrationId}/${clusterName}/${namespace}/${deploymentType}/${deploymentName}`);
161+
t.ok('dependencyGraphResults' in depGraphResult,
162+
'expected dependencyGraphResults field to exist in /dependency-graphs response');
163+
t.ok('imageMetadata' in JSON.parse(depGraphResult.dependencyGraphResults[imageName]),
164+
'snyk-monitor sent expected data to upstream in the expected timeframe');
165+
});
166+
167+
tap.test('snyk-monitor pulls images from a private ECR and sends data to homebase', async (t) => {
168+
if (process.env['TEST_PLATFORM'] !== 'eks') {
169+
t.pass('Not testing private ECR images because we\'re not running in EKS');
170+
return;
171+
}
172+
173+
t.plan(3);
174+
175+
const deploymentName = 'debian-ecr';
176+
const namespace = 'services';
177+
const clusterName = 'Default cluster';
178+
const deploymentType = WorkloadKind.Deployment;
179+
const imageName = '291964488713.dkr.ecr.us-east-2.amazonaws.com/snyk/debian';
180+
181+
await kubectl.applyK8sYaml('./test/fixtures/private-registries/debian-deployment-ecr.yaml');
182+
console.log(`Begin polling upstream for the expected private ECR image with integration ID ${integrationId}...`);
148183

149184
const validatorFn: WorkloadLocatorValidator = (workloads) => {
150185
return workloads !== undefined &&

0 commit comments

Comments
 (0)