Skip to content

Commit 800d492

Browse files
committed
Implement s3 artifact uploader for template
1 parent 689fc1b commit 800d492

27 files changed

+1418
-71
lines changed

package-lock.json

Lines changed: 275 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"@opentelemetry/sdk-trace-base": "2.0.1",
7272
"@opentelemetry/sdk-trace-node": "2.0.1",
7373
"@tree-sitter-grammars/tree-sitter-yaml": "0.7.1",
74+
"archiver": "^7.0.1",
7475
"axios": "1.11.0",
7576
"deep-object-diff": "1.1.9",
7677
"fast-deep-equal": "3.1.3",
@@ -97,6 +98,7 @@
9798
"devDependencies": {
9899
"@aws-sdk/types": "3.862.0",
99100
"@eslint/js": "9.34.0",
101+
"@types/archiver": "^7.0.0",
100102
"@types/js-yaml": "4.0.9",
101103
"@types/luxon": "3.7.1",
102104
"@types/yargs": "17.0.33",
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { readFileSync } from 'fs';
2+
import { resolve, dirname, isAbsolute } from 'path';
3+
import { fileURLToPath, pathToFileURL } from 'url';
4+
import { load } from 'js-yaml';
5+
import { TopLevelSection } from '../context/ContextType';
6+
import { Document, DocumentType } from '../document/Document';
7+
import { detectDocumentType } from '../document/DocumentUtils';
8+
import { S3Service } from '../services/S3Service';
9+
import { Artifact } from '../stacks/actions/StackActionRequestType';
10+
import { isS3Url, RESOURCE_EXPORTER_MAP } from './ResourceExporters';
11+
12+
export type ArtifactWithProperty = {
13+
resourceType: string;
14+
resourcePropertyDict: Record<string, unknown>;
15+
propertyName: string;
16+
localFilePath: string;
17+
};
18+
19+
export class Template {
20+
private readonly templateDict: unknown;
21+
private readonly templateUri: string;
22+
23+
constructor(
24+
private readonly s3Service: S3Service,
25+
private readonly bucketName: string,
26+
private readonly s3KeyPrefix: string = '',
27+
private readonly document?: Document,
28+
private readonly templateAbsPath?: string,
29+
) {
30+
if (this.document) {
31+
this.templateDict = this.document.getParsedDocumentContent();
32+
this.templateUri = this.document.uri;
33+
} else if (this.templateAbsPath) {
34+
const content = readFileSync(this.templateAbsPath, 'utf8');
35+
this.templateUri = pathToFileURL(this.templateAbsPath).href;
36+
const type = detectDocumentType(this.templateUri, content).type;
37+
if (type === DocumentType.YAML) {
38+
this.templateDict = load(content);
39+
} else {
40+
this.templateDict = JSON.parse(content);
41+
}
42+
} else {
43+
throw new Error('Either document or absolutePath must be provided');
44+
}
45+
}
46+
47+
private getResourceMapWithArtifact(): Record<string, ArtifactWithProperty[]> {
48+
const artifactMap: Record<string, ArtifactWithProperty[]> = {};
49+
50+
if (
51+
this.templateDict === undefined ||
52+
this.templateDict === null ||
53+
typeof this.templateDict !== 'object' ||
54+
!(TopLevelSection.Resources in this.templateDict)
55+
) {
56+
return artifactMap;
57+
}
58+
59+
const template = this.templateDict as Record<string, unknown>;
60+
const resources = template[TopLevelSection.Resources];
61+
62+
if (!resources || typeof resources !== 'object') {
63+
return artifactMap;
64+
}
65+
66+
const resourcesDict = resources as Record<string, unknown>;
67+
68+
for (const resourceObj of Object.values(resourcesDict)) {
69+
if (!resourceObj || typeof resourceObj !== 'object') continue;
70+
71+
const resource = resourceObj as Record<string, unknown>;
72+
const resourceType = resource.Type as string;
73+
74+
// Get the exporter class from the map
75+
const ExporterClass = RESOURCE_EXPORTER_MAP.get(resourceType);
76+
if (!ExporterClass) continue;
77+
78+
const properties = resource.Properties as Record<string, unknown> | undefined;
79+
if (properties) {
80+
const exporter = new ExporterClass(this.s3Service, this.bucketName, this.s3KeyPrefix);
81+
const propertyName = exporter.propertyName;
82+
const localFilePath = properties[propertyName];
83+
84+
if (typeof localFilePath === 'string') {
85+
if (!artifactMap[resourceType]) {
86+
artifactMap[resourceType] = [];
87+
}
88+
artifactMap[resourceType].push({
89+
resourceType,
90+
resourcePropertyDict: properties,
91+
propertyName,
92+
localFilePath,
93+
});
94+
}
95+
}
96+
}
97+
98+
return artifactMap;
99+
}
100+
101+
getTemplateArtifacts(): Artifact[] {
102+
const artifactMap = this.getResourceMapWithArtifact();
103+
const result: Artifact[] = [];
104+
105+
for (const [resourceType, artifacts] of Object.entries(artifactMap)) {
106+
for (const artifact of artifacts) {
107+
result.push({
108+
resourceType,
109+
filePath: artifact.localFilePath,
110+
});
111+
}
112+
}
113+
114+
return result;
115+
}
116+
117+
async export(): Promise<unknown> {
118+
if (
119+
this.templateDict === undefined ||
120+
this.templateDict === null ||
121+
typeof this.templateDict !== 'object' ||
122+
!(TopLevelSection.Resources in this.templateDict)
123+
) {
124+
return this.templateDict;
125+
}
126+
127+
await this.exportResources();
128+
return this.templateDict;
129+
}
130+
131+
private async exportResources(): Promise<void> {
132+
const artifactMap = this.getResourceMapWithArtifact();
133+
134+
for (const [resourceType, artifacts] of Object.entries(artifactMap)) {
135+
const ExporterClass = RESOURCE_EXPORTER_MAP.get(resourceType);
136+
137+
if (ExporterClass) {
138+
for (const artifact of artifacts) {
139+
if (
140+
isS3Url(artifact.localFilePath) ||
141+
artifact.localFilePath.startsWith('http://') ||
142+
artifact.localFilePath.startsWith('https://')
143+
) {
144+
// if filepath is not local path, skip uploading
145+
continue;
146+
}
147+
148+
const exporter = new ExporterClass(this.s3Service, this.bucketName, this.s3KeyPrefix);
149+
const templateUri = this.templateUri;
150+
const templatePath = templateUri.startsWith('file:') ? fileURLToPath(templateUri) : templateUri;
151+
const templateDir = dirname(templatePath);
152+
const artifactAbsPath = isAbsolute(artifact.localFilePath)
153+
? artifact.localFilePath
154+
: resolve(templateDir, artifact.localFilePath);
155+
await exporter.export(artifact.resourcePropertyDict, artifactAbsPath);
156+
}
157+
}
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)