Skip to content

Commit 376551a

Browse files
author
James Zhou
committed
feat(CG-1339): add aws ebs snapshot
1 parent 46a41e7 commit 376551a

File tree

16 files changed

+438
-0
lines changed

16 files changed

+438
-0
lines changed

src/enums/resources.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default {
2323
sqsQueue: 'aws_sqs_queue',
2424
iamGroup: 'aws_iam_group',
2525
snsTopic: 'aws_sns_topic',
26+
ebsSnapshot: 'aws_ebs_snapshot',
2627
ebsVolume: 'aws_ebs_volume',
2728
iamPolicy: 'aws_iam_policy',
2829
vpnGateway: 'aws_vpn_gateway',

src/enums/schemasMap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default {
3131
[services.dmsReplicationInstance]: 'awsDmsReplicationInstance',
3232
[services.dynamodb]: 'awsDynamoDbTable',
3333
[services.ebs]: 'awsEbs',
34+
[services.ebsSnapshot]: 'awsEbsSnapshot',
3435
[services.ec2Instance]: 'awsEc2',
3536
[services.ecr]: 'awsEcr',
3637
[services.ecsCluster]: 'awsEcsCluster',

src/enums/serviceAliases.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default {
1717
[services.codebuild]: 'codebuilds',
1818
[services.configurationRecorder]: 'configurationRecorders',
1919
[services.dmsReplicationInstance]: 'dmsReplicationInstances',
20+
[services.ebsSnapshot]: 'ebsSnapshots',
2021
[services.ec2Instance]: 'ec2Instances',
2122
[services.ecsCluster]: 'ecsClusters',
2223
[services.ecsContainer]: 'ecsContainers',

src/enums/serviceMap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import CognitoIdentityPool from '../services/cognitoIdentityPool'
2323
import CognitoUserPool from '../services/cognitoUserPool'
2424
import DynamoDB from '../services/dynamodb'
2525
import EBS from '../services/ebs'
26+
import EBSSnapshot from '../services/ebsSnapshot'
2627
import EC2 from '../services/ec2'
2728
import EcsCluster from '../services/ecsCluster'
2829
import EcsContainer from '../services/ecsContainer'
@@ -133,6 +134,7 @@ export default {
133134
[services.cognitoUserPool]: CognitoUserPool,
134135
[services.configurationRecorder]: ConfigurationRecorder,
135136
[services.ebs]: EBS,
137+
[services.ebsSnapshot]: EBSSnapshot,
136138
[services.ec2Instance]: EC2,
137139
[services.ecr]: ECR,
138140
[services.efs]: EFS,

src/enums/services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default {
2525
dmsReplicationInstance: 'dmsReplicationInstance',
2626
dynamodb: 'dynamodb',
2727
ebs: 'ebs',
28+
ebsSnapshot: 'ebsSnapshot',
2829
ec2Instance: 'ec2Instance',
2930
ecr: 'ecr',
3031
ecsCluster: 'ecsCluster',

src/properties/logger.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,13 @@ export default {
249249
doneFetchingEbsData: '✅ Done fetching EBS Data ✅',
250250
fetchedEbsVolumes: (num: number): string => `Fetched ${num} EBS Volumes`,
251251
lookingForEbs: 'Looking for EBS volumes for EC2 instances...',
252+
/**
253+
* EBS Snapshot
254+
*/
255+
fetchingEbsSnapshotData: 'Fetching EBS Snapshot data for this AWS account via the AWS SDK...',
256+
doneFetchingEbsSnapshotData: '✅ Done fetching EBS Snapshot Data ✅',
257+
fetchedEbsSnapshots: (num: number): string => `Fetched ${num} EBS Snapshots`,
258+
lookingForEbsSnapshot: 'Looking for EBS Snapshots...',
252259
/**
253260
* EC2
254261
*/

src/services/ebs/connections.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import isEmpty from 'lodash/isEmpty'
2+
3+
import EC2, {
4+
Volume,
5+
Snapshot,
6+
TagList,
7+
} from 'aws-sdk/clients/ec2'
8+
9+
import { ServiceConnection } from '@cloudgraph/sdk'
10+
11+
import services from '../../enums/services'
12+
13+
14+
/**
15+
* EBS
16+
*/
17+
18+
export default ({
19+
service: volume,
20+
data,
21+
region,
22+
account,
23+
}: {
24+
account: string
25+
data: { name: string; data: { [property: string]: any[] } }[]
26+
service: Volume & {
27+
region: string
28+
Tags?: TagList
29+
}
30+
region: string
31+
}): { [key: string]: ServiceConnection[] } => {
32+
const connections: ServiceConnection[] = []
33+
34+
const {
35+
VolumeId: id,
36+
SnapshotId: snapshotId,
37+
Tags: tags,
38+
} = volume
39+
40+
/**
41+
* Find EBS Snapshot
42+
* related to this EBS Volume
43+
*/
44+
const ebsSnapshots: {
45+
name: string
46+
data: { [property: string]: Snapshot[] }
47+
} = data.find(({ name }) => name === services.ebsSnapshot)
48+
49+
if (ebsSnapshots?.data?.[region]) {
50+
const snapshotInRegion: Snapshot[] = ebsSnapshots.data[region].filter(
51+
({ SnapshotId }: Snapshot) => SnapshotId === snapshotId
52+
)
53+
54+
if (!isEmpty(snapshotInRegion)) {
55+
for (const sh of snapshotInRegion) {
56+
connections.push({
57+
id: sh.SnapshotId,
58+
resourceType: services.ebsSnapshot,
59+
relation: 'child',
60+
field: 'ebsSnapshots',
61+
})
62+
}
63+
}
64+
}
65+
66+
const ebsResult = {
67+
[id]: connections,
68+
}
69+
return ebsResult
70+
}

src/services/ebs/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { Service } from '@cloudgraph/sdk'
22
import BaseService from '../base'
33
import format from './format'
44
import getData from './data'
5+
import getConnections from './connections'
56
import mutation from './mutation'
67

78
export default class EBS extends BaseService implements Service {
89
format = format.bind(this)
910

1011
getData = getData.bind(this)
1112

13+
getConnections = getConnections.bind(this)
14+
1215
mutation = mutation
1316
}

src/services/ebs/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type awsEbs implements awsBaseService @key(fields: "arn") {
1515
ec2Instance: [awsEc2] @hasInverse(field: ebs)
1616
asg: [awsAsg] @hasInverse(field: ebs)
1717
emrInstance: [awsEmrInstance] @hasInverse(field: ebs)
18+
ebsSnapshots: [awsEbsSnapshot] @hasInverse(field: ebs)
1819
}
1920

2021
type awsEbsAttachment

src/services/ebsSnapshot/data.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import EC2, {
2+
DescribeSnapshotsResult,
3+
DescribeSnapshotsRequest,
4+
Snapshot,
5+
CreateVolumePermission,
6+
DescribeSnapshotAttributeResult,
7+
} from 'aws-sdk/clients/ec2'
8+
import { Config } from 'aws-sdk/lib/config'
9+
import { AWSError } from 'aws-sdk/lib/error'
10+
11+
import groupBy from 'lodash/groupBy'
12+
import isEmpty from 'lodash/isEmpty'
13+
14+
import CloudGraph from '@cloudgraph/sdk'
15+
16+
import { AwsTag, TagMap } from '../../types'
17+
18+
import { initTestEndpoint } from '../../utils'
19+
import AwsErrorLog from '../../utils/errorLog'
20+
import { convertAwsTagsToTagMap } from '../../utils/format'
21+
import awsLoggerText from '../../properties/logger'
22+
23+
/**
24+
* EBS Snapshot
25+
*/
26+
27+
const lt = { ...awsLoggerText }
28+
const { logger } = CloudGraph
29+
const serviceName = 'EBS'
30+
const errorLog = new AwsErrorLog(serviceName)
31+
const endpoint = initTestEndpoint(serviceName)
32+
33+
export interface RawAwsEBSSnapshot extends Omit<Snapshot, 'Tags'> {
34+
region: string
35+
Permissions?: CreateVolumePermission[]
36+
Tags?: TagMap
37+
}
38+
39+
const listEbsSnapshotAttribute = async ({
40+
ec2,
41+
snapshotId,
42+
}: {
43+
ec2: EC2
44+
snapshotId: string
45+
}): Promise<CreateVolumePermission[]> =>
46+
new Promise(resolve =>
47+
ec2.describeSnapshotAttribute(
48+
{
49+
Attribute: 'createVolumePermission',
50+
SnapshotId: snapshotId,
51+
},
52+
(err: AWSError, data: DescribeSnapshotAttributeResult) => {
53+
if (err) {
54+
errorLog.generateAwsErrorLog({
55+
functionName: 'ec2:describeSnapshotAttribute',
56+
err,
57+
})
58+
}
59+
60+
if (isEmpty(data)) {
61+
return resolve([])
62+
}
63+
64+
return resolve(data?.CreateVolumePermissions)
65+
}
66+
)
67+
)
68+
69+
const listEbsSnapshots = async ({
70+
ec2,
71+
region,
72+
nextToken: NextToken = '',
73+
ebsSnapshotData,
74+
resolveRegion,
75+
}: {
76+
ec2: EC2
77+
region: string
78+
nextToken?: string
79+
ebsSnapshotData: RawAwsEBSSnapshot[]
80+
resolveRegion: () => void
81+
}): Promise<void> => {
82+
let args: DescribeSnapshotsRequest = {
83+
OwnerIds: ['self']
84+
}
85+
86+
if (NextToken) {
87+
args = { ...args, NextToken }
88+
}
89+
90+
ec2.describeSnapshots(
91+
args,
92+
async (err: AWSError, data: DescribeSnapshotsResult) => {
93+
if (err) {
94+
errorLog.generateAwsErrorLog({
95+
functionName: 'ec2:describeSnapshots',
96+
err,
97+
})
98+
}
99+
100+
/**
101+
* No EBS snapshot data for this region
102+
*/
103+
if (isEmpty(data)) {
104+
return resolveRegion()
105+
}
106+
107+
const { NextToken: nextToken, Snapshots: snapshots } = data || {}
108+
logger.debug(lt.fetchedEbsSnapshots(snapshots.length))
109+
110+
/**
111+
* No EBS Snapshot Found
112+
*/
113+
114+
if (isEmpty(snapshots)) {
115+
return resolveRegion()
116+
}
117+
118+
/**
119+
* Check to see if there are more
120+
*/
121+
122+
if (nextToken) {
123+
listEbsSnapshots({ region, nextToken, ec2, ebsSnapshotData, resolveRegion })
124+
}
125+
126+
const ebsVolumes = []
127+
128+
for (const { Tags, SnapshotId, ...snapshot } of snapshots) {
129+
let snapshotAttributes: CreateVolumePermission[] = []
130+
if (SnapshotId) {
131+
snapshotAttributes = await listEbsSnapshotAttribute({
132+
ec2,
133+
snapshotId: SnapshotId,
134+
})
135+
}
136+
ebsVolumes.push({
137+
...snapshot,
138+
region,
139+
SnapshotId,
140+
Permissions: snapshotAttributes,
141+
Tags: convertAwsTagsToTagMap(Tags as AwsTag[]),
142+
})
143+
}
144+
145+
ebsSnapshotData.push(...ebsVolumes)
146+
147+
/**
148+
* If this is the last page of data then return the instances
149+
*/
150+
151+
if (!nextToken) {
152+
resolveRegion()
153+
}
154+
}
155+
)
156+
}
157+
158+
export default async ({
159+
regions,
160+
config,
161+
}: {
162+
regions: string
163+
config: Config
164+
}): Promise<{
165+
[region: string]: Snapshot & { region: string }[]
166+
}> =>
167+
new Promise(async resolve => {
168+
const ebsSnapshotData: RawAwsEBSSnapshot[] = []
169+
const volumePromises: Promise<void>[] = []
170+
171+
// Get all the EBS data for each region with its snapshots
172+
for (const region of regions.split(',')) {
173+
const ec2 = new EC2({ ...config, region, endpoint })
174+
175+
volumePromises.push(
176+
new Promise<void>(resolveRegion =>
177+
listEbsSnapshots({ ec2, region, ebsSnapshotData, resolveRegion })
178+
)
179+
)
180+
}
181+
182+
logger.debug(lt.fetchingEbsSnapshotData)
183+
// Fetch EBS volumes
184+
await Promise.all(volumePromises)
185+
186+
errorLog.reset()
187+
188+
resolve(groupBy(ebsSnapshotData, 'region'))
189+
})

0 commit comments

Comments
 (0)