@stacksjs/ts-cloud 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws/client.d.ts +38 -0
- package/dist/aws/cloudfront.d.ts +5 -0
- package/dist/aws/index.d.ts +1 -0
- package/dist/aws/s3.d.ts +37 -3
- package/dist/aws/ses.d.ts +10 -1
- package/dist/bin/cli.js +6817 -4783
- package/dist/deploy/ensure-dynamic-cloudfront.d.ts +5 -0
- package/dist/deploy/migrate-site-stack.d.ts +31 -0
- package/dist/deploy/static-site-external-dns.d.ts +17 -0
- package/dist/drivers/aws/driver.d.ts +16 -0
- package/dist/drivers/factory.d.ts +17 -0
- package/dist/drivers/hetzner/client.d.ts +127 -0
- package/dist/drivers/hetzner/cloud-init.d.ts +17 -0
- package/dist/drivers/hetzner/driver.d.ts +28 -0
- package/dist/drivers/hetzner/firewall-rules.d.ts +12 -0
- package/dist/drivers/hetzner/instance-sizes.d.ts +10 -0
- package/dist/drivers/hetzner/state.d.ts +13 -0
- package/dist/drivers/index.d.ts +8 -0
- package/dist/drivers/shared/caddyfile.d.ts +6 -0
- package/dist/drivers/shared/compute-deploy.d.ts +25 -0
- package/dist/drivers/shared/deploy-script.d.ts +24 -0
- package/dist/generators/infrastructure.d.ts +17 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2803 -1374
- package/dist/object-storage/index.d.ts +69 -0
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1188,6 +1188,7 @@ var init_dist2 = __esm(() => {
|
|
|
1188
1188
|
// src/aws/client.ts
|
|
1189
1189
|
var exports_client = {};
|
|
1190
1190
|
__export(exports_client, {
|
|
1191
|
+
resolveS3Endpoint: () => resolveS3Endpoint,
|
|
1191
1192
|
detectCredentialSource: () => detectCredentialSource,
|
|
1192
1193
|
buildQueryParams: () => buildQueryParams,
|
|
1193
1194
|
AWSClient: () => AWSClient
|
|
@@ -1196,6 +1197,17 @@ import * as crypto2 from "node:crypto";
|
|
|
1196
1197
|
import { existsSync as existsSync14, readFileSync as readFileSync4 } from "node:fs";
|
|
1197
1198
|
import { homedir as homedir6 } from "node:os";
|
|
1198
1199
|
import { join as join9 } from "node:path";
|
|
1200
|
+
function resolveS3Endpoint(options) {
|
|
1201
|
+
const base = options.endpoint || `s3.${options.region}.amazonaws.com`;
|
|
1202
|
+
if (!options.bucket) {
|
|
1203
|
+
return { host: base, path: options.path };
|
|
1204
|
+
}
|
|
1205
|
+
if (options.forcePathStyle) {
|
|
1206
|
+
const path = options.path === "/" ? `/${options.bucket}` : `/${options.bucket}${options.path}`;
|
|
1207
|
+
return { host: base, path };
|
|
1208
|
+
}
|
|
1209
|
+
return { host: `${options.bucket}.${base}`, path: options.path };
|
|
1210
|
+
}
|
|
1199
1211
|
|
|
1200
1212
|
class AWSClient {
|
|
1201
1213
|
credentials;
|
|
@@ -1419,14 +1431,19 @@ class AWSClient {
|
|
|
1419
1431
|
return result;
|
|
1420
1432
|
}
|
|
1421
1433
|
buildUrl(options) {
|
|
1422
|
-
const { service, region,
|
|
1434
|
+
const { service, region, queryParams } = options;
|
|
1435
|
+
let { path } = options;
|
|
1423
1436
|
let host;
|
|
1424
1437
|
if (service === "s3") {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1438
|
+
const resolved = resolveS3Endpoint({
|
|
1439
|
+
region,
|
|
1440
|
+
path,
|
|
1441
|
+
bucket: options.bucket,
|
|
1442
|
+
endpoint: this.config.endpoint,
|
|
1443
|
+
forcePathStyle: this.config.forcePathStyle
|
|
1444
|
+
});
|
|
1445
|
+
host = resolved.host;
|
|
1446
|
+
path = resolved.path;
|
|
1430
1447
|
} else if (service === "cloudfront") {
|
|
1431
1448
|
host = "cloudfront.amazonaws.com";
|
|
1432
1449
|
} else if (service === "iam") {
|
|
@@ -1450,17 +1467,22 @@ class AWSClient {
|
|
|
1450
1467
|
return url;
|
|
1451
1468
|
}
|
|
1452
1469
|
signRequest(options, credentials) {
|
|
1453
|
-
const { service, region, method,
|
|
1470
|
+
const { service, region, method, queryParams, body } = options;
|
|
1471
|
+
let { path } = options;
|
|
1454
1472
|
const now = new Date;
|
|
1455
1473
|
const amzDate = this.getAmzDate(now);
|
|
1456
1474
|
const dateStamp = this.getDateStamp(now);
|
|
1457
1475
|
let host;
|
|
1458
1476
|
if (service === "s3") {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1477
|
+
const resolved = resolveS3Endpoint({
|
|
1478
|
+
region,
|
|
1479
|
+
path,
|
|
1480
|
+
bucket: options.bucket,
|
|
1481
|
+
endpoint: this.config.endpoint,
|
|
1482
|
+
forcePathStyle: this.config.forcePathStyle
|
|
1483
|
+
});
|
|
1484
|
+
host = resolved.host;
|
|
1485
|
+
path = resolved.path;
|
|
1464
1486
|
} else if (service === "cloudfront") {
|
|
1465
1487
|
host = "cloudfront.amazonaws.com";
|
|
1466
1488
|
} else if (service === "iam") {
|
|
@@ -1752,999 +1774,415 @@ var init_client = __esm(() => {
|
|
|
1752
1774
|
init_dist2();
|
|
1753
1775
|
});
|
|
1754
1776
|
|
|
1755
|
-
// src/aws/
|
|
1756
|
-
|
|
1777
|
+
// src/aws/credentials.ts
|
|
1778
|
+
function resolveCredentials2(profile) {
|
|
1779
|
+
if (profile) {
|
|
1780
|
+
const creds = loadProfileFromFile(profile);
|
|
1781
|
+
if (!creds) {
|
|
1782
|
+
throw new Error(`AWS profile '${profile}' not found in ~/.aws/credentials`);
|
|
1783
|
+
}
|
|
1784
|
+
return creds;
|
|
1785
|
+
}
|
|
1786
|
+
const envAccessKey = process.env.AWS_ACCESS_KEY_ID;
|
|
1787
|
+
const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
1788
|
+
if (envAccessKey && envSecretKey) {
|
|
1789
|
+
return {
|
|
1790
|
+
accessKeyId: envAccessKey,
|
|
1791
|
+
secretAccessKey: envSecretKey,
|
|
1792
|
+
sessionToken: process.env.AWS_SESSION_TOKEN
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
return loadProfileFromFile(process.env.AWS_PROFILE || "default") ?? { accessKeyId: "", secretAccessKey: "" };
|
|
1796
|
+
}
|
|
1797
|
+
function loadProfileFromFile(profile) {
|
|
1798
|
+
const { existsSync: existsSync15, readFileSync: readFileSync5 } = __require("node:fs");
|
|
1799
|
+
const { homedir: homedir5 } = __require("node:os");
|
|
1800
|
+
const { join: join8 } = __require("node:path");
|
|
1801
|
+
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join8(homedir5(), ".aws", "credentials");
|
|
1802
|
+
if (!existsSync15(credentialsPath))
|
|
1803
|
+
return null;
|
|
1804
|
+
const content = readFileSync5(credentialsPath, "utf-8");
|
|
1805
|
+
let currentProfile = null;
|
|
1806
|
+
let accessKeyId;
|
|
1807
|
+
let secretAccessKey;
|
|
1808
|
+
let sessionToken;
|
|
1809
|
+
for (const line of content.split(`
|
|
1810
|
+
`)) {
|
|
1811
|
+
const trimmed = line.trim();
|
|
1812
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
1813
|
+
continue;
|
|
1814
|
+
const profileMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
1815
|
+
if (profileMatch) {
|
|
1816
|
+
if (currentProfile === profile && accessKeyId && secretAccessKey) {
|
|
1817
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
1818
|
+
}
|
|
1819
|
+
currentProfile = profileMatch[1];
|
|
1820
|
+
accessKeyId = undefined;
|
|
1821
|
+
secretAccessKey = undefined;
|
|
1822
|
+
sessionToken = undefined;
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1825
|
+
if (currentProfile === profile) {
|
|
1826
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
1827
|
+
const value = valueParts.join("=").trim();
|
|
1828
|
+
switch (key.trim().toLowerCase()) {
|
|
1829
|
+
case "aws_access_key_id":
|
|
1830
|
+
accessKeyId = value;
|
|
1831
|
+
break;
|
|
1832
|
+
case "aws_secret_access_key":
|
|
1833
|
+
secretAccessKey = value;
|
|
1834
|
+
break;
|
|
1835
|
+
case "aws_session_token":
|
|
1836
|
+
sessionToken = value;
|
|
1837
|
+
break;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (currentProfile === profile && accessKeyId && secretAccessKey) {
|
|
1842
|
+
return { accessKeyId, secretAccessKey, sessionToken };
|
|
1843
|
+
}
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// src/aws/s3.ts
|
|
1848
|
+
import * as crypto3 from "node:crypto";
|
|
1849
|
+
import { readdir as readdir4 } from "node:fs/promises";
|
|
1850
|
+
import { join as join10 } from "node:path";
|
|
1851
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1852
|
+
function toFetchBody(data) {
|
|
1853
|
+
const { buffer, byteOffset, byteLength } = data;
|
|
1854
|
+
if (buffer instanceof ArrayBuffer) {
|
|
1855
|
+
return buffer.slice(byteOffset, byteOffset + byteLength);
|
|
1856
|
+
}
|
|
1857
|
+
const copy = new ArrayBuffer(byteLength);
|
|
1858
|
+
new Uint8Array(copy).set(new Uint8Array(data));
|
|
1859
|
+
return copy;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
class S3Client2 {
|
|
1757
1863
|
client;
|
|
1758
1864
|
region;
|
|
1759
|
-
|
|
1865
|
+
explicitProfile;
|
|
1866
|
+
endpoint;
|
|
1867
|
+
forcePathStyle;
|
|
1868
|
+
explicitCredentials;
|
|
1869
|
+
constructor(region = "us-east-1", profile, options) {
|
|
1760
1870
|
this.region = region;
|
|
1761
|
-
this.
|
|
1871
|
+
this.explicitProfile = profile;
|
|
1872
|
+
this.endpoint = options?.endpoint;
|
|
1873
|
+
this.forcePathStyle = options?.forcePathStyle;
|
|
1874
|
+
this.explicitCredentials = options?.credentials;
|
|
1875
|
+
this.client = new AWSClient(options?.credentials ?? resolveCredentials2(profile), { endpoint: options?.endpoint, forcePathStyle: options?.forcePathStyle });
|
|
1762
1876
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
StackName: options.stackName,
|
|
1767
|
-
Version: "2010-05-15"
|
|
1768
|
-
};
|
|
1769
|
-
if (options.templateBody) {
|
|
1770
|
-
params.TemplateBody = options.templateBody;
|
|
1771
|
-
} else if (options.templateUrl) {
|
|
1772
|
-
params.TemplateURL = options.templateUrl;
|
|
1773
|
-
} else {
|
|
1774
|
-
throw new Error("Either templateBody or templateUrl must be provided");
|
|
1775
|
-
}
|
|
1776
|
-
if (options.parameters) {
|
|
1777
|
-
options.parameters.forEach((param, index) => {
|
|
1778
|
-
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
|
|
1779
|
-
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
|
|
1780
|
-
});
|
|
1877
|
+
getCredentials() {
|
|
1878
|
+
if (this.explicitCredentials?.accessKeyId && this.explicitCredentials.secretAccessKey) {
|
|
1879
|
+
return this.explicitCredentials;
|
|
1781
1880
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
});
|
|
1881
|
+
const creds = resolveCredentials2(this.explicitProfile);
|
|
1882
|
+
if (creds.accessKeyId && creds.secretAccessKey) {
|
|
1883
|
+
return creds;
|
|
1786
1884
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1885
|
+
throw new Error("S3 credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (or pass explicit credentials/profile), or configure ~/.aws/credentials.");
|
|
1886
|
+
}
|
|
1887
|
+
s3BaseHost() {
|
|
1888
|
+
return this.endpoint || `s3.${this.region}.amazonaws.com`;
|
|
1889
|
+
}
|
|
1890
|
+
s3VirtualHost(bucket) {
|
|
1891
|
+
return this.forcePathStyle ? this.s3BaseHost() : `${bucket}.${this.s3BaseHost()}`;
|
|
1892
|
+
}
|
|
1893
|
+
async listBuckets() {
|
|
1894
|
+
const result = await this.client.request({
|
|
1895
|
+
service: "s3",
|
|
1896
|
+
region: this.region,
|
|
1897
|
+
method: "GET",
|
|
1898
|
+
path: "/"
|
|
1899
|
+
});
|
|
1900
|
+
const buckets = [];
|
|
1901
|
+
const root = result?.ListAllMyBucketsResult ?? result;
|
|
1902
|
+
const bucketList = root?.Buckets?.Bucket;
|
|
1903
|
+
if (bucketList) {
|
|
1904
|
+
const list = Array.isArray(bucketList) ? bucketList : [bucketList];
|
|
1905
|
+
for (const b of list) {
|
|
1906
|
+
buckets.push({
|
|
1907
|
+
Name: b.Name,
|
|
1908
|
+
CreationDate: b.CreationDate
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1789
1911
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1912
|
+
return { Buckets: buckets };
|
|
1913
|
+
}
|
|
1914
|
+
async createBucket(bucket, options) {
|
|
1915
|
+
const headers = {};
|
|
1916
|
+
if (options?.acl) {
|
|
1917
|
+
headers["x-amz-acl"] = options.acl;
|
|
1795
1918
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1919
|
+
let body;
|
|
1920
|
+
if (this.region !== "us-east-1" && !this.endpoint) {
|
|
1921
|
+
body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1922
|
+
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
1923
|
+
<LocationConstraint>${this.region}</LocationConstraint>
|
|
1924
|
+
</CreateBucketConfiguration>`;
|
|
1925
|
+
headers["Content-Type"] = "application/xml";
|
|
1798
1926
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1927
|
+
await this.client.request({
|
|
1928
|
+
service: "s3",
|
|
1929
|
+
region: this.region,
|
|
1930
|
+
method: "PUT",
|
|
1931
|
+
path: `/${bucket}`,
|
|
1932
|
+
headers,
|
|
1933
|
+
body
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
async deleteBucket(bucket) {
|
|
1937
|
+
await this.client.request({
|
|
1938
|
+
service: "s3",
|
|
1939
|
+
region: this.region,
|
|
1940
|
+
method: "DELETE",
|
|
1941
|
+
path: `/${bucket}`
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
async emptyAndDeleteBucket(bucket) {
|
|
1945
|
+
let hasMore = true;
|
|
1946
|
+
while (hasMore) {
|
|
1947
|
+
const objects = await this.listAllObjects({ bucket });
|
|
1948
|
+
if (objects.length === 0) {
|
|
1949
|
+
hasMore = false;
|
|
1950
|
+
break;
|
|
1951
|
+
}
|
|
1952
|
+
const keys = objects.map((obj) => obj.Key);
|
|
1953
|
+
for (let i = 0;i < keys.length; i += 1000) {
|
|
1954
|
+
const batch2 = keys.slice(i, i + 1000);
|
|
1955
|
+
await this.deleteObjects(bucket, batch2);
|
|
1956
|
+
}
|
|
1801
1957
|
}
|
|
1958
|
+
await this.deleteBucket(bucket);
|
|
1959
|
+
}
|
|
1960
|
+
async listAllObjects(options) {
|
|
1961
|
+
const allObjects = [];
|
|
1962
|
+
let continuationToken;
|
|
1963
|
+
do {
|
|
1964
|
+
const params = {
|
|
1965
|
+
"list-type": "2",
|
|
1966
|
+
"max-keys": "1000"
|
|
1967
|
+
};
|
|
1968
|
+
if (options.prefix) {
|
|
1969
|
+
params.prefix = options.prefix;
|
|
1970
|
+
}
|
|
1971
|
+
if (continuationToken) {
|
|
1972
|
+
params["continuation-token"] = continuationToken;
|
|
1973
|
+
}
|
|
1974
|
+
const result = await this.client.request({
|
|
1975
|
+
service: "s3",
|
|
1976
|
+
region: this.region,
|
|
1977
|
+
method: "GET",
|
|
1978
|
+
path: `/${options.bucket}`,
|
|
1979
|
+
queryParams: params
|
|
1980
|
+
});
|
|
1981
|
+
const root = result?.ListBucketResult ?? result;
|
|
1982
|
+
const contents = root?.Contents;
|
|
1983
|
+
if (contents) {
|
|
1984
|
+
const list = Array.isArray(contents) ? contents : [contents];
|
|
1985
|
+
for (const obj of list) {
|
|
1986
|
+
allObjects.push({
|
|
1987
|
+
Key: obj.Key,
|
|
1988
|
+
LastModified: obj.LastModified || "",
|
|
1989
|
+
Size: Number.parseInt(obj.Size || "0"),
|
|
1990
|
+
ETag: obj.ETag
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
const isTruncated = root?.IsTruncated;
|
|
1995
|
+
continuationToken = isTruncated === "true" || isTruncated === true ? root?.NextContinuationToken : undefined;
|
|
1996
|
+
} while (continuationToken);
|
|
1997
|
+
return allObjects;
|
|
1998
|
+
}
|
|
1999
|
+
async list(options) {
|
|
1802
2000
|
const result = await this.client.request({
|
|
1803
|
-
service: "
|
|
2001
|
+
service: "s3",
|
|
1804
2002
|
region: this.region,
|
|
1805
|
-
method: "
|
|
1806
|
-
path:
|
|
1807
|
-
body: new URLSearchParams(params).toString()
|
|
2003
|
+
method: "GET",
|
|
2004
|
+
path: `/${options.bucket}`
|
|
1808
2005
|
});
|
|
1809
|
-
|
|
2006
|
+
const objects = [];
|
|
2007
|
+
const root = result?.ListBucketResult ?? result;
|
|
2008
|
+
const contents = root?.Contents;
|
|
2009
|
+
if (contents) {
|
|
2010
|
+
const items = Array.isArray(contents) ? contents : [contents];
|
|
2011
|
+
for (const item of items) {
|
|
2012
|
+
if (options.prefix && !item.Key?.startsWith(options.prefix)) {
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
objects.push({
|
|
2016
|
+
Key: item.Key || "",
|
|
2017
|
+
LastModified: item.LastModified || "",
|
|
2018
|
+
Size: Number.parseInt(item.Size || "0"),
|
|
2019
|
+
ETag: item.ETag
|
|
2020
|
+
});
|
|
2021
|
+
if (options.maxKeys && objects.length >= options.maxKeys) {
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return objects;
|
|
1810
2027
|
}
|
|
1811
|
-
async
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
Version: "2010-05-15"
|
|
1816
|
-
};
|
|
1817
|
-
if (options.templateBody) {
|
|
1818
|
-
params.TemplateBody = options.templateBody;
|
|
1819
|
-
} else if (options.templateUrl) {
|
|
1820
|
-
params.TemplateURL = options.templateUrl;
|
|
2028
|
+
async putObject(options) {
|
|
2029
|
+
const headers = {};
|
|
2030
|
+
if (options.acl) {
|
|
2031
|
+
headers["x-amz-acl"] = options.acl;
|
|
1821
2032
|
}
|
|
1822
|
-
if (options.
|
|
1823
|
-
options.
|
|
1824
|
-
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
|
|
1825
|
-
if (param.UsePreviousValue) {
|
|
1826
|
-
params[`Parameters.member.${index + 1}.UsePreviousValue`] = "true";
|
|
1827
|
-
} else {
|
|
1828
|
-
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
|
|
1829
|
-
}
|
|
1830
|
-
});
|
|
2033
|
+
if (options.cacheControl) {
|
|
2034
|
+
headers["Cache-Control"] = options.cacheControl;
|
|
1831
2035
|
}
|
|
1832
|
-
if (options.
|
|
1833
|
-
options.
|
|
1834
|
-
params[`Capabilities.member.${index + 1}`] = cap;
|
|
1835
|
-
});
|
|
2036
|
+
if (options.contentType) {
|
|
2037
|
+
headers["Content-Type"] = options.contentType;
|
|
1836
2038
|
}
|
|
1837
|
-
if (options.
|
|
1838
|
-
|
|
2039
|
+
if (options.metadata) {
|
|
2040
|
+
for (const [key, value] of Object.entries(options.metadata)) {
|
|
2041
|
+
headers[`x-amz-meta-${key}`] = value;
|
|
2042
|
+
}
|
|
1839
2043
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2044
|
+
const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body) ? Buffer.from(options.body) : options.body;
|
|
2045
|
+
if (Buffer.isBuffer(normalizedBody) || normalizedBody instanceof Uint8Array) {
|
|
2046
|
+
const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody);
|
|
2047
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
2048
|
+
const host = this.s3VirtualHost(options.bucket);
|
|
2049
|
+
const url = `https://${host}/${options.key}`;
|
|
2050
|
+
const now = new Date;
|
|
2051
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
2052
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2053
|
+
const payloadHash = crypto3.createHash("sha256").update(binaryBody).digest("hex");
|
|
2054
|
+
const requestHeaders = {
|
|
2055
|
+
host,
|
|
2056
|
+
"x-amz-date": amzDate,
|
|
2057
|
+
"x-amz-content-sha256": payloadHash,
|
|
2058
|
+
...headers
|
|
2059
|
+
};
|
|
2060
|
+
if (sessionToken) {
|
|
2061
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
2062
|
+
}
|
|
2063
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
|
|
2064
|
+
`).join("");
|
|
2065
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
|
|
2066
|
+
const canonicalRequest = [
|
|
2067
|
+
"PUT",
|
|
2068
|
+
`/${options.key}`,
|
|
2069
|
+
"",
|
|
2070
|
+
canonicalHeaders,
|
|
2071
|
+
signedHeaders,
|
|
2072
|
+
payloadHash
|
|
2073
|
+
].join(`
|
|
2074
|
+
`);
|
|
2075
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
2076
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
2077
|
+
const stringToSign = [
|
|
2078
|
+
algorithm,
|
|
2079
|
+
amzDate,
|
|
2080
|
+
credentialScope,
|
|
2081
|
+
crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
2082
|
+
].join(`
|
|
2083
|
+
`);
|
|
2084
|
+
const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
2085
|
+
const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
|
|
2086
|
+
const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
|
|
2087
|
+
const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
|
|
2088
|
+
const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
2089
|
+
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
2090
|
+
const response = await fetch(url, {
|
|
2091
|
+
method: "PUT",
|
|
2092
|
+
headers: {
|
|
2093
|
+
...requestHeaders,
|
|
2094
|
+
Authorization: authorizationHeader
|
|
2095
|
+
},
|
|
2096
|
+
body: toFetchBody(binaryBody)
|
|
1844
2097
|
});
|
|
2098
|
+
if (!response.ok) {
|
|
2099
|
+
const errorText = await response.text();
|
|
2100
|
+
throw new Error(`S3 PUT failed: ${response.status} ${errorText}`);
|
|
2101
|
+
}
|
|
2102
|
+
return;
|
|
1845
2103
|
}
|
|
2104
|
+
await this.client.request({
|
|
2105
|
+
service: "s3",
|
|
2106
|
+
region: this.region,
|
|
2107
|
+
method: "PUT",
|
|
2108
|
+
path: `/${options.key}`,
|
|
2109
|
+
bucket: options.bucket,
|
|
2110
|
+
headers,
|
|
2111
|
+
body: options.body
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
async getObject(bucket, key) {
|
|
1846
2115
|
const result = await this.client.request({
|
|
1847
|
-
service: "
|
|
2116
|
+
service: "s3",
|
|
1848
2117
|
region: this.region,
|
|
1849
|
-
method: "
|
|
1850
|
-
path:
|
|
1851
|
-
|
|
2118
|
+
method: "GET",
|
|
2119
|
+
path: `/${bucket}/${key}`,
|
|
2120
|
+
rawResponse: true
|
|
1852
2121
|
});
|
|
1853
|
-
return
|
|
2122
|
+
return result;
|
|
1854
2123
|
}
|
|
1855
|
-
async
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
StackName: stackName,
|
|
1859
|
-
Version: "2010-05-15"
|
|
2124
|
+
async copyObject(options) {
|
|
2125
|
+
const headers = {
|
|
2126
|
+
"x-amz-copy-source": `/${options.sourceBucket}/${options.sourceKey}`
|
|
1860
2127
|
};
|
|
1861
|
-
if (
|
|
1862
|
-
|
|
2128
|
+
if (options.metadataDirective) {
|
|
2129
|
+
headers["x-amz-metadata-directive"] = options.metadataDirective;
|
|
1863
2130
|
}
|
|
1864
|
-
if (
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2131
|
+
if (options.contentType) {
|
|
2132
|
+
headers["Content-Type"] = options.contentType;
|
|
2133
|
+
}
|
|
2134
|
+
if (options.metadata) {
|
|
2135
|
+
for (const [key, value] of Object.entries(options.metadata)) {
|
|
2136
|
+
headers[`x-amz-meta-${key}`] = value;
|
|
2137
|
+
}
|
|
1868
2138
|
}
|
|
1869
2139
|
await this.client.request({
|
|
1870
|
-
service: "
|
|
2140
|
+
service: "s3",
|
|
1871
2141
|
region: this.region,
|
|
1872
|
-
method: "
|
|
1873
|
-
path:
|
|
1874
|
-
|
|
2142
|
+
method: "PUT",
|
|
2143
|
+
path: `/${options.destinationBucket}/${options.destinationKey}`,
|
|
2144
|
+
headers
|
|
1875
2145
|
});
|
|
1876
2146
|
}
|
|
1877
|
-
async
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
Version: "2010-05-15"
|
|
1881
|
-
};
|
|
1882
|
-
if (options.stackName) {
|
|
1883
|
-
params.StackName = options.stackName;
|
|
1884
|
-
}
|
|
1885
|
-
const result = await this.client.request({
|
|
1886
|
-
service: "cloudformation",
|
|
2147
|
+
async deleteObject(bucket, key) {
|
|
2148
|
+
await this.client.request({
|
|
2149
|
+
service: "s3",
|
|
1887
2150
|
region: this.region,
|
|
1888
|
-
method: "
|
|
1889
|
-
path:
|
|
1890
|
-
body: new URLSearchParams(params).toString()
|
|
2151
|
+
method: "DELETE",
|
|
2152
|
+
path: `/${bucket}/${key}`
|
|
1891
2153
|
});
|
|
1892
|
-
const stacks = this.parseStacksResponse(result);
|
|
1893
|
-
return { Stacks: stacks };
|
|
1894
2154
|
}
|
|
1895
|
-
async
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
const
|
|
1902
|
-
|
|
2155
|
+
async deleteObjects(bucket, keys) {
|
|
2156
|
+
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2157
|
+
<Delete>
|
|
2158
|
+
${keys.map((key) => `<Object><Key>${key}</Key></Object>`).join(`
|
|
2159
|
+
`)}
|
|
2160
|
+
</Delete>`;
|
|
2161
|
+
const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
|
|
2162
|
+
await this.client.request({
|
|
2163
|
+
service: "s3",
|
|
1903
2164
|
region: this.region,
|
|
1904
2165
|
method: "POST",
|
|
1905
|
-
path:
|
|
1906
|
-
|
|
2166
|
+
path: `/${bucket}`,
|
|
2167
|
+
queryParams: { delete: "" },
|
|
2168
|
+
body: deleteXml,
|
|
2169
|
+
headers: {
|
|
2170
|
+
"Content-Type": "application/xml",
|
|
2171
|
+
"Content-MD5": contentMd5
|
|
2172
|
+
}
|
|
1907
2173
|
});
|
|
1908
|
-
return { StackEvents: this.parseStackEvents(result) };
|
|
1909
2174
|
}
|
|
1910
|
-
async
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
body: new URLSearchParams(params).toString()
|
|
1922
|
-
});
|
|
1923
|
-
const member = result?.ListStackResourcesResult?.StackResourceSummaries?.member;
|
|
1924
|
-
let resources = [];
|
|
1925
|
-
if (member) {
|
|
1926
|
-
resources = Array.isArray(member) ? member : [member];
|
|
1927
|
-
}
|
|
1928
|
-
return { StackResourceSummaries: resources };
|
|
1929
|
-
}
|
|
1930
|
-
async waitForStack(stackName, waitType) {
|
|
1931
|
-
const targetStatuses = {
|
|
1932
|
-
"stack-create-complete": ["CREATE_COMPLETE"],
|
|
1933
|
-
"stack-update-complete": ["UPDATE_COMPLETE"],
|
|
1934
|
-
"stack-delete-complete": ["DELETE_COMPLETE"]
|
|
1935
|
-
};
|
|
1936
|
-
const failureStatuses = [
|
|
1937
|
-
"CREATE_FAILED",
|
|
1938
|
-
"ROLLBACK_FAILED",
|
|
1939
|
-
"ROLLBACK_COMPLETE",
|
|
1940
|
-
"UPDATE_ROLLBACK_FAILED",
|
|
1941
|
-
"UPDATE_ROLLBACK_COMPLETE"
|
|
1942
|
-
];
|
|
1943
|
-
const targets = targetStatuses[waitType];
|
|
1944
|
-
const maxAttempts = 360;
|
|
1945
|
-
let attempts = 0;
|
|
1946
|
-
while (attempts < maxAttempts) {
|
|
1947
|
-
try {
|
|
1948
|
-
const result = await this.describeStacks({ stackName });
|
|
1949
|
-
if (result.Stacks.length === 0) {
|
|
1950
|
-
if (waitType === "stack-delete-complete") {
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
if (attempts % 10 === 0) {
|
|
1954
|
-
console.log(`[waitForStack] Attempt ${attempts}: Stack not visible yet`);
|
|
1955
|
-
}
|
|
1956
|
-
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
1957
|
-
attempts++;
|
|
1958
|
-
continue;
|
|
1959
|
-
}
|
|
1960
|
-
const stack = result.Stacks[0];
|
|
1961
|
-
if (attempts % 10 === 0) {
|
|
1962
|
-
console.log(`[waitForStack] Attempt ${attempts}: Status = ${stack.StackStatus}${stack.StackStatusReason ? ` (${stack.StackStatusReason})` : ""}`);
|
|
1963
|
-
}
|
|
1964
|
-
if (targets.includes(stack.StackStatus)) {
|
|
1965
|
-
return;
|
|
1966
|
-
}
|
|
1967
|
-
if ((waitType === "stack-create-complete" || waitType === "stack-update-complete") && (stack.StackStatus === "DELETE_IN_PROGRESS" || stack.StackStatus === "DELETE_COMPLETE")) {
|
|
1968
|
-
console.log(`[waitForStack] Stack is being deleted (creation/update failed)`);
|
|
1969
|
-
let failedEventReason = "";
|
|
1970
|
-
try {
|
|
1971
|
-
const eventsResult = await this.describeStackEvents(stackName);
|
|
1972
|
-
console.log("[waitForStack] Stack events (most recent first):");
|
|
1973
|
-
for (const event of eventsResult.StackEvents.slice(0, 15)) {
|
|
1974
|
-
if (event.ResourceStatus.includes("FAILED") || event.ResourceStatusReason) {
|
|
1975
|
-
console.log(` ${event.LogicalResourceId}: ${event.ResourceStatus} - ${event.ResourceStatusReason || "No reason provided"}`);
|
|
1976
|
-
if (event.ResourceStatus.includes("FAILED") && event.ResourceStatusReason && !failedEventReason) {
|
|
1977
|
-
failedEventReason = event.ResourceStatusReason;
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
} catch {}
|
|
1982
|
-
const errorReason = failedEventReason || stack.StackStatusReason || "Check CloudFormation console for details.";
|
|
1983
|
-
throw new Error(`Stack creation/update failed - stack is being deleted. Reason: ${errorReason}`);
|
|
1984
|
-
}
|
|
1985
|
-
if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
|
|
1986
|
-
const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
|
|
1987
|
-
error.code = "DELETE_FAILED";
|
|
1988
|
-
error.stackStatus = stack.StackStatus;
|
|
1989
|
-
error.statusReason = stack.StackStatusReason;
|
|
1990
|
-
throw error;
|
|
1991
|
-
}
|
|
1992
|
-
if (failureStatuses.includes(stack.StackStatus)) {
|
|
1993
|
-
throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
|
|
1994
|
-
}
|
|
1995
|
-
await new Promise((resolve13) => setTimeout(resolve13, 5000));
|
|
1996
|
-
attempts++;
|
|
1997
|
-
} catch (error) {
|
|
1998
|
-
if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
|
|
1999
|
-
return;
|
|
2000
|
-
}
|
|
2001
|
-
if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
|
|
2002
|
-
if (attempts % 10 === 0) {
|
|
2003
|
-
console.log(`[waitForStack] Attempt ${attempts}: Stack does not exist (error), retrying...`);
|
|
2004
|
-
}
|
|
2005
|
-
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
2006
|
-
attempts++;
|
|
2007
|
-
continue;
|
|
2008
|
-
}
|
|
2009
|
-
console.log(`[waitForStack] Unexpected error: ${error.message}`);
|
|
2010
|
-
throw error;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
console.log(`[waitForStack] Timeout after ${attempts} attempts`);
|
|
2014
|
-
throw new Error(`Timeout waiting for stack to reach ${waitType}`);
|
|
2015
|
-
}
|
|
2016
|
-
async validateTemplate(templateBody) {
|
|
2017
|
-
const params = {
|
|
2018
|
-
Action: "ValidateTemplate",
|
|
2019
|
-
TemplateBody: templateBody,
|
|
2020
|
-
Version: "2010-05-15"
|
|
2021
|
-
};
|
|
2022
|
-
return await this.client.request({
|
|
2023
|
-
service: "cloudformation",
|
|
2024
|
-
region: this.region,
|
|
2025
|
-
method: "POST",
|
|
2026
|
-
path: "/",
|
|
2027
|
-
body: new URLSearchParams(params).toString()
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
async listStacks(statusFilter) {
|
|
2031
|
-
const params = {
|
|
2032
|
-
Action: "ListStacks",
|
|
2033
|
-
Version: "2010-05-15"
|
|
2034
|
-
};
|
|
2035
|
-
if (statusFilter) {
|
|
2036
|
-
statusFilter.forEach((status, index) => {
|
|
2037
|
-
params[`StackStatusFilter.member.${index + 1}`] = status;
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
const result = await this.client.request({
|
|
2041
|
-
service: "cloudformation",
|
|
2042
|
-
region: this.region,
|
|
2043
|
-
method: "POST",
|
|
2044
|
-
path: "/",
|
|
2045
|
-
body: new URLSearchParams(params).toString()
|
|
2046
|
-
});
|
|
2047
|
-
const response = result.ListStacksResponse || result;
|
|
2048
|
-
const summariesResult = response.ListStacksResult || response;
|
|
2049
|
-
const members = summariesResult.StackSummaries?.member || summariesResult.StackSummaries || [];
|
|
2050
|
-
const items = Array.isArray(members) ? members : members ? [members] : [];
|
|
2051
|
-
return {
|
|
2052
|
-
StackSummaries: items.map((s) => ({
|
|
2053
|
-
StackId: s.StackId || "",
|
|
2054
|
-
StackName: s.StackName || "",
|
|
2055
|
-
TemplateDescription: s.TemplateDescription,
|
|
2056
|
-
CreationTime: s.CreationTime || "",
|
|
2057
|
-
LastUpdatedTime: s.LastUpdatedTime,
|
|
2058
|
-
DeletionTime: s.DeletionTime,
|
|
2059
|
-
StackStatus: s.StackStatus || ""
|
|
2060
|
-
}))
|
|
2061
|
-
};
|
|
2062
|
-
}
|
|
2063
|
-
async createChangeSet(options) {
|
|
2064
|
-
const params = {
|
|
2065
|
-
Action: "CreateChangeSet",
|
|
2066
|
-
StackName: options.stackName,
|
|
2067
|
-
ChangeSetName: options.changeSetName,
|
|
2068
|
-
Version: "2010-05-15"
|
|
2069
|
-
};
|
|
2070
|
-
if (options.templateBody) {
|
|
2071
|
-
params.TemplateBody = options.templateBody;
|
|
2072
|
-
} else if (options.templateUrl) {
|
|
2073
|
-
params.TemplateURL = options.templateUrl;
|
|
2074
|
-
}
|
|
2075
|
-
if (options.parameters) {
|
|
2076
|
-
options.parameters.forEach((param, index) => {
|
|
2077
|
-
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
|
|
2078
|
-
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
if (options.capabilities) {
|
|
2082
|
-
options.capabilities.forEach((cap, index) => {
|
|
2083
|
-
params[`Capabilities.member.${index + 1}`] = cap;
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
if (options.changeSetType) {
|
|
2087
|
-
params.ChangeSetType = options.changeSetType;
|
|
2088
|
-
}
|
|
2089
|
-
const result = await this.client.request({
|
|
2090
|
-
service: "cloudformation",
|
|
2091
|
-
region: this.region,
|
|
2092
|
-
method: "POST",
|
|
2093
|
-
path: "/",
|
|
2094
|
-
body: new URLSearchParams(params).toString()
|
|
2095
|
-
});
|
|
2096
|
-
return { Id: result.Id, StackId: result.StackId };
|
|
2097
|
-
}
|
|
2098
|
-
async describeChangeSet(stackName, changeSetName) {
|
|
2099
|
-
const params = {
|
|
2100
|
-
Action: "DescribeChangeSet",
|
|
2101
|
-
StackName: stackName,
|
|
2102
|
-
ChangeSetName: changeSetName,
|
|
2103
|
-
Version: "2010-05-15"
|
|
2104
|
-
};
|
|
2105
|
-
return await this.client.request({
|
|
2106
|
-
service: "cloudformation",
|
|
2107
|
-
region: this.region,
|
|
2108
|
-
method: "POST",
|
|
2109
|
-
path: "/",
|
|
2110
|
-
body: new URLSearchParams(params).toString()
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
2113
|
-
async executeChangeSet(stackName, changeSetName) {
|
|
2114
|
-
const params = {
|
|
2115
|
-
Action: "ExecuteChangeSet",
|
|
2116
|
-
StackName: stackName,
|
|
2117
|
-
ChangeSetName: changeSetName,
|
|
2118
|
-
Version: "2010-05-15"
|
|
2119
|
-
};
|
|
2120
|
-
await this.client.request({
|
|
2121
|
-
service: "cloudformation",
|
|
2122
|
-
region: this.region,
|
|
2123
|
-
method: "POST",
|
|
2124
|
-
path: "/",
|
|
2125
|
-
body: new URLSearchParams(params).toString()
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
async deleteChangeSet(stackName, changeSetName) {
|
|
2129
|
-
const params = {
|
|
2130
|
-
Action: "DeleteChangeSet",
|
|
2131
|
-
StackName: stackName,
|
|
2132
|
-
ChangeSetName: changeSetName,
|
|
2133
|
-
Version: "2010-05-15"
|
|
2134
|
-
};
|
|
2135
|
-
await this.client.request({
|
|
2136
|
-
service: "cloudformation",
|
|
2137
|
-
region: this.region,
|
|
2138
|
-
method: "POST",
|
|
2139
|
-
path: "/",
|
|
2140
|
-
body: new URLSearchParams(params).toString()
|
|
2141
|
-
});
|
|
2142
|
-
}
|
|
2143
|
-
async getStackOutputs(stackName) {
|
|
2144
|
-
const result = await this.describeStacks({ stackName });
|
|
2145
|
-
if (!result.Stacks || result.Stacks.length === 0) {
|
|
2146
|
-
throw new Error(`Stack ${stackName} not found`);
|
|
2147
|
-
}
|
|
2148
|
-
const stack = result.Stacks[0];
|
|
2149
|
-
const outputs = {};
|
|
2150
|
-
if (stack.Outputs) {
|
|
2151
|
-
for (const output of stack.Outputs) {
|
|
2152
|
-
outputs[output.OutputKey] = output.OutputValue;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
return outputs;
|
|
2156
|
-
}
|
|
2157
|
-
async getTemplate(stackName) {
|
|
2158
|
-
const params = {
|
|
2159
|
-
Action: "GetTemplate",
|
|
2160
|
-
StackName: stackName,
|
|
2161
|
-
Version: "2010-05-15"
|
|
2162
|
-
};
|
|
2163
|
-
const result = await this.client.request({
|
|
2164
|
-
service: "cloudformation",
|
|
2165
|
-
region: this.region,
|
|
2166
|
-
method: "POST",
|
|
2167
|
-
path: "/",
|
|
2168
|
-
body: new URLSearchParams(params).toString()
|
|
2169
|
-
});
|
|
2170
|
-
const templateBody = result?.GetTemplateResult?.TemplateBody || result?.TemplateBody || "";
|
|
2171
|
-
return { TemplateBody: templateBody };
|
|
2172
|
-
}
|
|
2173
|
-
parseStacksResponse(result) {
|
|
2174
|
-
const stacks = [];
|
|
2175
|
-
let stackData = result?.DescribeStacksResult?.Stacks?.member || result?.Stacks?.member || result?.Stacks || result;
|
|
2176
|
-
if (stackData && !Array.isArray(stackData)) {
|
|
2177
|
-
stackData = [stackData];
|
|
2178
|
-
}
|
|
2179
|
-
if (Array.isArray(stackData)) {
|
|
2180
|
-
for (const s of stackData) {
|
|
2181
|
-
if (s.StackId || s.StackName) {
|
|
2182
|
-
let outputs;
|
|
2183
|
-
if (s.Outputs?.member) {
|
|
2184
|
-
const outputData = Array.isArray(s.Outputs.member) ? s.Outputs.member : [s.Outputs.member];
|
|
2185
|
-
outputs = outputData.map((o) => ({
|
|
2186
|
-
OutputKey: o.OutputKey,
|
|
2187
|
-
OutputValue: o.OutputValue,
|
|
2188
|
-
Description: o.Description,
|
|
2189
|
-
ExportName: o.ExportName
|
|
2190
|
-
}));
|
|
2191
|
-
}
|
|
2192
|
-
stacks.push({
|
|
2193
|
-
StackId: s.StackId,
|
|
2194
|
-
StackName: s.StackName,
|
|
2195
|
-
StackStatus: s.StackStatus,
|
|
2196
|
-
CreationTime: s.CreationTime,
|
|
2197
|
-
LastUpdatedTime: s.LastUpdatedTime,
|
|
2198
|
-
StackStatusReason: s.StackStatusReason,
|
|
2199
|
-
Outputs: outputs
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
} else if (result.StackId) {
|
|
2204
|
-
stacks.push({
|
|
2205
|
-
StackId: result.StackId,
|
|
2206
|
-
StackName: result.StackName,
|
|
2207
|
-
StackStatus: result.StackStatus,
|
|
2208
|
-
CreationTime: result.CreationTime,
|
|
2209
|
-
LastUpdatedTime: result.LastUpdatedTime
|
|
2210
|
-
});
|
|
2211
|
-
}
|
|
2212
|
-
return stacks;
|
|
2213
|
-
}
|
|
2214
|
-
parseStackEvents(result) {
|
|
2215
|
-
const events = [];
|
|
2216
|
-
let eventData = result?.DescribeStackEventsResult?.StackEvents?.member || result?.StackEvents?.member || result?.StackEvents || [];
|
|
2217
|
-
if (eventData && !Array.isArray(eventData)) {
|
|
2218
|
-
eventData = [eventData];
|
|
2219
|
-
}
|
|
2220
|
-
for (const e of eventData) {
|
|
2221
|
-
if (e.LogicalResourceId) {
|
|
2222
|
-
events.push({
|
|
2223
|
-
Timestamp: e.Timestamp,
|
|
2224
|
-
ResourceType: e.ResourceType,
|
|
2225
|
-
LogicalResourceId: e.LogicalResourceId,
|
|
2226
|
-
ResourceStatus: e.ResourceStatus,
|
|
2227
|
-
ResourceStatusReason: e.ResourceStatusReason
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
return events;
|
|
2232
|
-
}
|
|
2233
|
-
async waitForStackWithProgress(stackName, waitType, onProgress) {
|
|
2234
|
-
const targetStatuses = {
|
|
2235
|
-
"stack-create-complete": ["CREATE_COMPLETE"],
|
|
2236
|
-
"stack-update-complete": ["UPDATE_COMPLETE"],
|
|
2237
|
-
"stack-delete-complete": ["DELETE_COMPLETE"]
|
|
2238
|
-
};
|
|
2239
|
-
const failureStatuses = [
|
|
2240
|
-
"CREATE_FAILED",
|
|
2241
|
-
"ROLLBACK_FAILED",
|
|
2242
|
-
"ROLLBACK_COMPLETE",
|
|
2243
|
-
"UPDATE_ROLLBACK_FAILED",
|
|
2244
|
-
"UPDATE_ROLLBACK_COMPLETE"
|
|
2245
|
-
];
|
|
2246
|
-
const targets = targetStatuses[waitType];
|
|
2247
|
-
const maxAttempts = 360;
|
|
2248
|
-
let attempts = 0;
|
|
2249
|
-
const seenEvents = new Set;
|
|
2250
|
-
while (attempts < maxAttempts) {
|
|
2251
|
-
try {
|
|
2252
|
-
if (onProgress) {
|
|
2253
|
-
try {
|
|
2254
|
-
const eventsResult = await this.describeStackEvents(stackName);
|
|
2255
|
-
const events = [...eventsResult.StackEvents || []].reverse();
|
|
2256
|
-
for (const event of events) {
|
|
2257
|
-
const eventKey = `${event.LogicalResourceId}-${event.ResourceStatus}-${event.Timestamp}`;
|
|
2258
|
-
if (!seenEvents.has(eventKey)) {
|
|
2259
|
-
seenEvents.add(eventKey);
|
|
2260
|
-
onProgress({
|
|
2261
|
-
resourceId: event.LogicalResourceId,
|
|
2262
|
-
resourceType: event.ResourceType,
|
|
2263
|
-
status: event.ResourceStatus,
|
|
2264
|
-
reason: event.ResourceStatusReason,
|
|
2265
|
-
timestamp: event.Timestamp
|
|
2266
|
-
});
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
} catch {}
|
|
2270
|
-
}
|
|
2271
|
-
const result = await this.describeStacks({ stackName });
|
|
2272
|
-
if (result.Stacks.length === 0) {
|
|
2273
|
-
if (waitType === "stack-delete-complete") {
|
|
2274
|
-
return;
|
|
2275
|
-
}
|
|
2276
|
-
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
2277
|
-
attempts++;
|
|
2278
|
-
continue;
|
|
2279
|
-
}
|
|
2280
|
-
const stack = result.Stacks[0];
|
|
2281
|
-
if (targets.includes(stack.StackStatus)) {
|
|
2282
|
-
return;
|
|
2283
|
-
}
|
|
2284
|
-
if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
|
|
2285
|
-
const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
|
|
2286
|
-
error.code = "DELETE_FAILED";
|
|
2287
|
-
error.stackStatus = stack.StackStatus;
|
|
2288
|
-
error.statusReason = stack.StackStatusReason;
|
|
2289
|
-
throw error;
|
|
2290
|
-
}
|
|
2291
|
-
if (failureStatuses.includes(stack.StackStatus)) {
|
|
2292
|
-
throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
|
|
2293
|
-
}
|
|
2294
|
-
await new Promise((resolve13) => setTimeout(resolve13, 3000));
|
|
2295
|
-
attempts++;
|
|
2296
|
-
} catch (error) {
|
|
2297
|
-
if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
|
|
2298
|
-
return;
|
|
2299
|
-
}
|
|
2300
|
-
if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
|
|
2301
|
-
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
2302
|
-
attempts++;
|
|
2303
|
-
continue;
|
|
2304
|
-
}
|
|
2305
|
-
throw error;
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
throw new Error(`Timeout waiting for stack to reach ${waitType}`);
|
|
2309
|
-
}
|
|
2310
|
-
async waitForStackComplete(stackName, maxAttempts = 120, delayMs = 5000) {
|
|
2311
|
-
const successStatuses = [
|
|
2312
|
-
"CREATE_COMPLETE",
|
|
2313
|
-
"UPDATE_COMPLETE",
|
|
2314
|
-
"DELETE_COMPLETE"
|
|
2315
|
-
];
|
|
2316
|
-
const failureStatuses = [
|
|
2317
|
-
"CREATE_FAILED",
|
|
2318
|
-
"UPDATE_FAILED",
|
|
2319
|
-
"DELETE_FAILED",
|
|
2320
|
-
"ROLLBACK_COMPLETE",
|
|
2321
|
-
"ROLLBACK_FAILED",
|
|
2322
|
-
"UPDATE_ROLLBACK_COMPLETE",
|
|
2323
|
-
"UPDATE_ROLLBACK_FAILED"
|
|
2324
|
-
];
|
|
2325
|
-
for (let i = 0;i < maxAttempts; i++) {
|
|
2326
|
-
try {
|
|
2327
|
-
const result = await this.describeStacks({ stackName });
|
|
2328
|
-
if (result.Stacks.length === 0) {
|
|
2329
|
-
return { success: true, status: "DELETE_COMPLETE" };
|
|
2330
|
-
}
|
|
2331
|
-
const stack = result.Stacks[0];
|
|
2332
|
-
const status = stack.StackStatus;
|
|
2333
|
-
if (successStatuses.includes(status)) {
|
|
2334
|
-
return { success: true, status };
|
|
2335
|
-
}
|
|
2336
|
-
if (failureStatuses.includes(status)) {
|
|
2337
|
-
return { success: false, status, reason: stack.StackStatusReason };
|
|
2338
|
-
}
|
|
2339
|
-
await new Promise((resolve13) => setTimeout(resolve13, delayMs));
|
|
2340
|
-
} catch (error) {
|
|
2341
|
-
if (error.message?.includes("does not exist")) {
|
|
2342
|
-
return { success: true, status: "DELETE_COMPLETE" };
|
|
2343
|
-
}
|
|
2344
|
-
throw error;
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
return { success: false, status: "TIMEOUT", reason: "Timeout waiting for stack operation" };
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
var init_cloudformation = __esm(() => {
|
|
2351
|
-
init_client();
|
|
2352
|
-
});
|
|
2353
|
-
|
|
2354
|
-
// src/aws/credentials.ts
|
|
2355
|
-
function resolveCredentials2(profile) {
|
|
2356
|
-
if (profile) {
|
|
2357
|
-
const creds = loadProfileFromFile(profile);
|
|
2358
|
-
if (!creds) {
|
|
2359
|
-
throw new Error(`AWS profile '${profile}' not found in ~/.aws/credentials`);
|
|
2360
|
-
}
|
|
2361
|
-
return creds;
|
|
2362
|
-
}
|
|
2363
|
-
const envAccessKey = process.env.AWS_ACCESS_KEY_ID;
|
|
2364
|
-
const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
2365
|
-
if (envAccessKey && envSecretKey) {
|
|
2366
|
-
return {
|
|
2367
|
-
accessKeyId: envAccessKey,
|
|
2368
|
-
secretAccessKey: envSecretKey,
|
|
2369
|
-
sessionToken: process.env.AWS_SESSION_TOKEN
|
|
2370
|
-
};
|
|
2371
|
-
}
|
|
2372
|
-
return loadProfileFromFile(process.env.AWS_PROFILE || "default") ?? { accessKeyId: "", secretAccessKey: "" };
|
|
2373
|
-
}
|
|
2374
|
-
function loadProfileFromFile(profile) {
|
|
2375
|
-
const { existsSync: existsSync15, readFileSync: readFileSync5 } = __require("node:fs");
|
|
2376
|
-
const { homedir: homedir5 } = __require("node:os");
|
|
2377
|
-
const { join: join8 } = __require("node:path");
|
|
2378
|
-
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join8(homedir5(), ".aws", "credentials");
|
|
2379
|
-
if (!existsSync15(credentialsPath))
|
|
2380
|
-
return null;
|
|
2381
|
-
const content = readFileSync5(credentialsPath, "utf-8");
|
|
2382
|
-
let currentProfile = null;
|
|
2383
|
-
let accessKeyId;
|
|
2384
|
-
let secretAccessKey;
|
|
2385
|
-
let sessionToken;
|
|
2386
|
-
for (const line of content.split(`
|
|
2387
|
-
`)) {
|
|
2388
|
-
const trimmed = line.trim();
|
|
2389
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
2390
|
-
continue;
|
|
2391
|
-
const profileMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
2392
|
-
if (profileMatch) {
|
|
2393
|
-
if (currentProfile === profile && accessKeyId && secretAccessKey) {
|
|
2394
|
-
return { accessKeyId, secretAccessKey, sessionToken };
|
|
2395
|
-
}
|
|
2396
|
-
currentProfile = profileMatch[1];
|
|
2397
|
-
accessKeyId = undefined;
|
|
2398
|
-
secretAccessKey = undefined;
|
|
2399
|
-
sessionToken = undefined;
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
if (currentProfile === profile) {
|
|
2403
|
-
const [key, ...valueParts] = trimmed.split("=");
|
|
2404
|
-
const value = valueParts.join("=").trim();
|
|
2405
|
-
switch (key.trim().toLowerCase()) {
|
|
2406
|
-
case "aws_access_key_id":
|
|
2407
|
-
accessKeyId = value;
|
|
2408
|
-
break;
|
|
2409
|
-
case "aws_secret_access_key":
|
|
2410
|
-
secretAccessKey = value;
|
|
2411
|
-
break;
|
|
2412
|
-
case "aws_session_token":
|
|
2413
|
-
sessionToken = value;
|
|
2414
|
-
break;
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
if (currentProfile === profile && accessKeyId && secretAccessKey) {
|
|
2419
|
-
return { accessKeyId, secretAccessKey, sessionToken };
|
|
2420
|
-
}
|
|
2421
|
-
return null;
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
// src/aws/s3.ts
|
|
2425
|
-
import * as crypto3 from "node:crypto";
|
|
2426
|
-
import { readdir as readdir4 } from "node:fs/promises";
|
|
2427
|
-
import { join as join10 } from "node:path";
|
|
2428
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2429
|
-
function toFetchBody(data) {
|
|
2430
|
-
const { buffer, byteOffset, byteLength } = data;
|
|
2431
|
-
if (buffer instanceof ArrayBuffer) {
|
|
2432
|
-
return buffer.slice(byteOffset, byteOffset + byteLength);
|
|
2433
|
-
}
|
|
2434
|
-
const copy = new ArrayBuffer(byteLength);
|
|
2435
|
-
new Uint8Array(copy).set(new Uint8Array(data));
|
|
2436
|
-
return copy;
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
class S3Client2 {
|
|
2440
|
-
client;
|
|
2441
|
-
region;
|
|
2442
|
-
explicitProfile;
|
|
2443
|
-
constructor(region = "us-east-1", profile) {
|
|
2444
|
-
this.region = region;
|
|
2445
|
-
this.explicitProfile = profile;
|
|
2446
|
-
this.client = new AWSClient(resolveCredentials2(profile));
|
|
2447
|
-
}
|
|
2448
|
-
getCredentials() {
|
|
2449
|
-
const creds = resolveCredentials2(this.explicitProfile);
|
|
2450
|
-
if (creds.accessKeyId && creds.secretAccessKey) {
|
|
2451
|
-
return creds;
|
|
2452
|
-
}
|
|
2453
|
-
throw new Error("AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, pass an explicit profile, or configure ~/.aws/credentials.");
|
|
2454
|
-
}
|
|
2455
|
-
async listBuckets() {
|
|
2456
|
-
const result = await this.client.request({
|
|
2457
|
-
service: "s3",
|
|
2458
|
-
region: this.region,
|
|
2459
|
-
method: "GET",
|
|
2460
|
-
path: "/"
|
|
2461
|
-
});
|
|
2462
|
-
const buckets = [];
|
|
2463
|
-
const root = result?.ListAllMyBucketsResult ?? result;
|
|
2464
|
-
const bucketList = root?.Buckets?.Bucket;
|
|
2465
|
-
if (bucketList) {
|
|
2466
|
-
const list = Array.isArray(bucketList) ? bucketList : [bucketList];
|
|
2467
|
-
for (const b of list) {
|
|
2468
|
-
buckets.push({
|
|
2469
|
-
Name: b.Name,
|
|
2470
|
-
CreationDate: b.CreationDate
|
|
2471
|
-
});
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
return { Buckets: buckets };
|
|
2475
|
-
}
|
|
2476
|
-
async createBucket(bucket, options) {
|
|
2477
|
-
const headers = {};
|
|
2478
|
-
if (options?.acl) {
|
|
2479
|
-
headers["x-amz-acl"] = options.acl;
|
|
2480
|
-
}
|
|
2481
|
-
let body;
|
|
2482
|
-
if (this.region !== "us-east-1") {
|
|
2483
|
-
body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2484
|
-
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
2485
|
-
<LocationConstraint>${this.region}</LocationConstraint>
|
|
2486
|
-
</CreateBucketConfiguration>`;
|
|
2487
|
-
headers["Content-Type"] = "application/xml";
|
|
2488
|
-
}
|
|
2489
|
-
await this.client.request({
|
|
2490
|
-
service: "s3",
|
|
2491
|
-
region: this.region,
|
|
2492
|
-
method: "PUT",
|
|
2493
|
-
path: `/${bucket}`,
|
|
2494
|
-
headers,
|
|
2495
|
-
body
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
|
-
async deleteBucket(bucket) {
|
|
2499
|
-
await this.client.request({
|
|
2500
|
-
service: "s3",
|
|
2501
|
-
region: this.region,
|
|
2502
|
-
method: "DELETE",
|
|
2503
|
-
path: `/${bucket}`
|
|
2504
|
-
});
|
|
2505
|
-
}
|
|
2506
|
-
async emptyAndDeleteBucket(bucket) {
|
|
2507
|
-
let hasMore = true;
|
|
2508
|
-
while (hasMore) {
|
|
2509
|
-
const objects = await this.listAllObjects({ bucket });
|
|
2510
|
-
if (objects.length === 0) {
|
|
2511
|
-
hasMore = false;
|
|
2512
|
-
break;
|
|
2513
|
-
}
|
|
2514
|
-
const keys = objects.map((obj) => obj.Key);
|
|
2515
|
-
for (let i = 0;i < keys.length; i += 1000) {
|
|
2516
|
-
const batch2 = keys.slice(i, i + 1000);
|
|
2517
|
-
await this.deleteObjects(bucket, batch2);
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
await this.deleteBucket(bucket);
|
|
2521
|
-
}
|
|
2522
|
-
async listAllObjects(options) {
|
|
2523
|
-
const allObjects = [];
|
|
2524
|
-
let continuationToken;
|
|
2525
|
-
do {
|
|
2526
|
-
const params = {
|
|
2527
|
-
"list-type": "2",
|
|
2528
|
-
"max-keys": "1000"
|
|
2529
|
-
};
|
|
2530
|
-
if (options.prefix) {
|
|
2531
|
-
params.prefix = options.prefix;
|
|
2532
|
-
}
|
|
2533
|
-
if (continuationToken) {
|
|
2534
|
-
params["continuation-token"] = continuationToken;
|
|
2535
|
-
}
|
|
2536
|
-
const result = await this.client.request({
|
|
2537
|
-
service: "s3",
|
|
2538
|
-
region: this.region,
|
|
2539
|
-
method: "GET",
|
|
2540
|
-
path: `/${options.bucket}`,
|
|
2541
|
-
queryParams: params
|
|
2542
|
-
});
|
|
2543
|
-
const root = result?.ListBucketResult ?? result;
|
|
2544
|
-
const contents = root?.Contents;
|
|
2545
|
-
if (contents) {
|
|
2546
|
-
const list = Array.isArray(contents) ? contents : [contents];
|
|
2547
|
-
for (const obj of list) {
|
|
2548
|
-
allObjects.push({
|
|
2549
|
-
Key: obj.Key,
|
|
2550
|
-
LastModified: obj.LastModified || "",
|
|
2551
|
-
Size: Number.parseInt(obj.Size || "0"),
|
|
2552
|
-
ETag: obj.ETag
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
const isTruncated = root?.IsTruncated;
|
|
2557
|
-
continuationToken = isTruncated === "true" || isTruncated === true ? root?.NextContinuationToken : undefined;
|
|
2558
|
-
} while (continuationToken);
|
|
2559
|
-
return allObjects;
|
|
2560
|
-
}
|
|
2561
|
-
async list(options) {
|
|
2562
|
-
const result = await this.client.request({
|
|
2563
|
-
service: "s3",
|
|
2564
|
-
region: this.region,
|
|
2565
|
-
method: "GET",
|
|
2566
|
-
path: `/${options.bucket}`
|
|
2567
|
-
});
|
|
2568
|
-
const objects = [];
|
|
2569
|
-
const root = result?.ListBucketResult ?? result;
|
|
2570
|
-
const contents = root?.Contents;
|
|
2571
|
-
if (contents) {
|
|
2572
|
-
const items = Array.isArray(contents) ? contents : [contents];
|
|
2573
|
-
for (const item of items) {
|
|
2574
|
-
if (options.prefix && !item.Key?.startsWith(options.prefix)) {
|
|
2575
|
-
continue;
|
|
2576
|
-
}
|
|
2577
|
-
objects.push({
|
|
2578
|
-
Key: item.Key || "",
|
|
2579
|
-
LastModified: item.LastModified || "",
|
|
2580
|
-
Size: Number.parseInt(item.Size || "0"),
|
|
2581
|
-
ETag: item.ETag
|
|
2582
|
-
});
|
|
2583
|
-
if (options.maxKeys && objects.length >= options.maxKeys) {
|
|
2584
|
-
break;
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
return objects;
|
|
2589
|
-
}
|
|
2590
|
-
async putObject(options) {
|
|
2591
|
-
const headers = {};
|
|
2592
|
-
if (options.acl) {
|
|
2593
|
-
headers["x-amz-acl"] = options.acl;
|
|
2594
|
-
}
|
|
2595
|
-
if (options.cacheControl) {
|
|
2596
|
-
headers["Cache-Control"] = options.cacheControl;
|
|
2597
|
-
}
|
|
2598
|
-
if (options.contentType) {
|
|
2599
|
-
headers["Content-Type"] = options.contentType;
|
|
2600
|
-
}
|
|
2601
|
-
if (options.metadata) {
|
|
2602
|
-
for (const [key, value] of Object.entries(options.metadata)) {
|
|
2603
|
-
headers[`x-amz-meta-${key}`] = value;
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body) ? Buffer.from(options.body) : options.body;
|
|
2607
|
-
if (Buffer.isBuffer(normalizedBody) || normalizedBody instanceof Uint8Array) {
|
|
2608
|
-
const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody);
|
|
2609
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
2610
|
-
const host = `${options.bucket}.s3.${this.region}.amazonaws.com`;
|
|
2611
|
-
const url = `https://${host}/${options.key}`;
|
|
2612
|
-
const now = new Date;
|
|
2613
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
2614
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2615
|
-
const payloadHash = crypto3.createHash("sha256").update(binaryBody).digest("hex");
|
|
2616
|
-
const requestHeaders = {
|
|
2617
|
-
host,
|
|
2618
|
-
"x-amz-date": amzDate,
|
|
2619
|
-
"x-amz-content-sha256": payloadHash,
|
|
2620
|
-
...headers
|
|
2621
|
-
};
|
|
2622
|
-
if (sessionToken) {
|
|
2623
|
-
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
2624
|
-
}
|
|
2625
|
-
const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
|
|
2626
|
-
`).join("");
|
|
2627
|
-
const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
|
|
2628
|
-
const canonicalRequest = [
|
|
2629
|
-
"PUT",
|
|
2630
|
-
`/${options.key}`,
|
|
2631
|
-
"",
|
|
2632
|
-
canonicalHeaders,
|
|
2633
|
-
signedHeaders,
|
|
2634
|
-
payloadHash
|
|
2635
|
-
].join(`
|
|
2636
|
-
`);
|
|
2637
|
-
const algorithm = "AWS4-HMAC-SHA256";
|
|
2638
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
2639
|
-
const stringToSign = [
|
|
2640
|
-
algorithm,
|
|
2641
|
-
amzDate,
|
|
2642
|
-
credentialScope,
|
|
2643
|
-
crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
2644
|
-
].join(`
|
|
2645
|
-
`);
|
|
2646
|
-
const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
2647
|
-
const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
|
|
2648
|
-
const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
|
|
2649
|
-
const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
|
|
2650
|
-
const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
2651
|
-
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
2652
|
-
const response = await fetch(url, {
|
|
2653
|
-
method: "PUT",
|
|
2654
|
-
headers: {
|
|
2655
|
-
...requestHeaders,
|
|
2656
|
-
Authorization: authorizationHeader
|
|
2657
|
-
},
|
|
2658
|
-
body: toFetchBody(binaryBody)
|
|
2659
|
-
});
|
|
2660
|
-
if (!response.ok) {
|
|
2661
|
-
const errorText = await response.text();
|
|
2662
|
-
throw new Error(`S3 PUT failed: ${response.status} ${errorText}`);
|
|
2663
|
-
}
|
|
2664
|
-
return;
|
|
2665
|
-
}
|
|
2666
|
-
await this.client.request({
|
|
2667
|
-
service: "s3",
|
|
2668
|
-
region: this.region,
|
|
2669
|
-
method: "PUT",
|
|
2670
|
-
path: `/${options.key}`,
|
|
2671
|
-
bucket: options.bucket,
|
|
2672
|
-
headers,
|
|
2673
|
-
body: options.body
|
|
2674
|
-
});
|
|
2675
|
-
}
|
|
2676
|
-
async getObject(bucket, key) {
|
|
2677
|
-
const result = await this.client.request({
|
|
2678
|
-
service: "s3",
|
|
2679
|
-
region: this.region,
|
|
2680
|
-
method: "GET",
|
|
2681
|
-
path: `/${bucket}/${key}`,
|
|
2682
|
-
rawResponse: true
|
|
2683
|
-
});
|
|
2684
|
-
return result;
|
|
2685
|
-
}
|
|
2686
|
-
async copyObject(options) {
|
|
2687
|
-
const headers = {
|
|
2688
|
-
"x-amz-copy-source": `/${options.sourceBucket}/${options.sourceKey}`
|
|
2689
|
-
};
|
|
2690
|
-
if (options.metadataDirective) {
|
|
2691
|
-
headers["x-amz-metadata-directive"] = options.metadataDirective;
|
|
2692
|
-
}
|
|
2693
|
-
if (options.contentType) {
|
|
2694
|
-
headers["Content-Type"] = options.contentType;
|
|
2695
|
-
}
|
|
2696
|
-
if (options.metadata) {
|
|
2697
|
-
for (const [key, value] of Object.entries(options.metadata)) {
|
|
2698
|
-
headers[`x-amz-meta-${key}`] = value;
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
await this.client.request({
|
|
2702
|
-
service: "s3",
|
|
2703
|
-
region: this.region,
|
|
2704
|
-
method: "PUT",
|
|
2705
|
-
path: `/${options.destinationBucket}/${options.destinationKey}`,
|
|
2706
|
-
headers
|
|
2707
|
-
});
|
|
2708
|
-
}
|
|
2709
|
-
async deleteObject(bucket, key) {
|
|
2710
|
-
await this.client.request({
|
|
2711
|
-
service: "s3",
|
|
2712
|
-
region: this.region,
|
|
2713
|
-
method: "DELETE",
|
|
2714
|
-
path: `/${bucket}/${key}`
|
|
2715
|
-
});
|
|
2716
|
-
}
|
|
2717
|
-
async deleteObjects(bucket, keys) {
|
|
2718
|
-
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2719
|
-
<Delete>
|
|
2720
|
-
${keys.map((key) => `<Object><Key>${key}</Key></Object>`).join(`
|
|
2721
|
-
`)}
|
|
2722
|
-
</Delete>`;
|
|
2723
|
-
const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
|
|
2724
|
-
await this.client.request({
|
|
2725
|
-
service: "s3",
|
|
2726
|
-
region: this.region,
|
|
2727
|
-
method: "POST",
|
|
2728
|
-
path: `/${bucket}`,
|
|
2729
|
-
queryParams: { delete: "" },
|
|
2730
|
-
body: deleteXml,
|
|
2731
|
-
headers: {
|
|
2732
|
-
"Content-Type": "application/xml",
|
|
2733
|
-
"Content-MD5": contentMd5
|
|
2734
|
-
}
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
async bucketExists(bucket) {
|
|
2738
|
-
try {
|
|
2739
|
-
await this.client.request({
|
|
2740
|
-
service: "s3",
|
|
2741
|
-
region: this.region,
|
|
2742
|
-
method: "HEAD",
|
|
2743
|
-
path: `/${bucket}`
|
|
2744
|
-
});
|
|
2745
|
-
return true;
|
|
2746
|
-
} catch {
|
|
2747
|
-
return false;
|
|
2175
|
+
async bucketExists(bucket) {
|
|
2176
|
+
try {
|
|
2177
|
+
await this.client.request({
|
|
2178
|
+
service: "s3",
|
|
2179
|
+
region: this.region,
|
|
2180
|
+
method: "HEAD",
|
|
2181
|
+
path: `/${bucket}`
|
|
2182
|
+
});
|
|
2183
|
+
return true;
|
|
2184
|
+
} catch {
|
|
2185
|
+
return false;
|
|
2748
2186
|
}
|
|
2749
2187
|
}
|
|
2750
2188
|
async copy(options) {
|
|
@@ -2814,7 +2252,7 @@ class S3Client2 {
|
|
|
2814
2252
|
}
|
|
2815
2253
|
async putBucketPolicy(bucket, policy) {
|
|
2816
2254
|
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
2817
|
-
const host =
|
|
2255
|
+
const host = this.s3BaseHost();
|
|
2818
2256
|
const policyString = typeof policy === "string" ? policy : JSON.stringify(policy);
|
|
2819
2257
|
const now = new Date;
|
|
2820
2258
|
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
@@ -2874,7 +2312,7 @@ class S3Client2 {
|
|
|
2874
2312
|
}
|
|
2875
2313
|
async getBucketPolicy(bucket) {
|
|
2876
2314
|
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
2877
|
-
const host =
|
|
2315
|
+
const host = this.s3BaseHost();
|
|
2878
2316
|
const now = new Date;
|
|
2879
2317
|
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
2880
2318
|
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
@@ -3565,7 +3003,7 @@ class S3Client2 {
|
|
|
3565
3003
|
}
|
|
3566
3004
|
generatePresignedGetUrl(bucket, key, expiresInSeconds = 3600) {
|
|
3567
3005
|
const { accessKeyId, secretAccessKey } = this.getCredentials();
|
|
3568
|
-
const host =
|
|
3006
|
+
const host = this.s3VirtualHost(bucket);
|
|
3569
3007
|
const now = new Date;
|
|
3570
3008
|
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
3571
3009
|
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
@@ -3605,7 +3043,7 @@ class S3Client2 {
|
|
|
3605
3043
|
}
|
|
3606
3044
|
generatePresignedPutUrl(bucket, key, contentType, expiresInSeconds = 3600) {
|
|
3607
3045
|
const { accessKeyId, secretAccessKey } = this.getCredentials();
|
|
3608
|
-
const host =
|
|
3046
|
+
const host = this.s3VirtualHost(bucket);
|
|
3609
3047
|
const now = new Date;
|
|
3610
3048
|
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
3611
3049
|
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
@@ -3649,371 +3087,970 @@ host:${host}
|
|
|
3649
3087
|
if (options?.contentType) {
|
|
3650
3088
|
headers["Content-Type"] = options.contentType;
|
|
3651
3089
|
}
|
|
3652
|
-
if (options?.metadata) {
|
|
3653
|
-
for (const [k, v] of Object.entries(options.metadata)) {
|
|
3654
|
-
headers[`x-amz-meta-${k}`] = v;
|
|
3655
|
-
}
|
|
3090
|
+
if (options?.metadata) {
|
|
3091
|
+
for (const [k, v] of Object.entries(options.metadata)) {
|
|
3092
|
+
headers[`x-amz-meta-${k}`] = v;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
const result = await this.client.request({
|
|
3096
|
+
service: "s3",
|
|
3097
|
+
region: this.region,
|
|
3098
|
+
method: "POST",
|
|
3099
|
+
path: `/${bucket}/${key}`,
|
|
3100
|
+
queryParams: { uploads: "" },
|
|
3101
|
+
headers
|
|
3102
|
+
});
|
|
3103
|
+
return { UploadId: result?.InitiateMultipartUploadResult?.UploadId };
|
|
3104
|
+
}
|
|
3105
|
+
async uploadPart(bucket, key, uploadId, partNumber, body) {
|
|
3106
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
3107
|
+
const host = this.s3VirtualHost(bucket);
|
|
3108
|
+
const url = `https://${host}/${key}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`;
|
|
3109
|
+
const now = new Date;
|
|
3110
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
3111
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
3112
|
+
const payloadHash = crypto3.createHash("sha256").update(body).digest("hex");
|
|
3113
|
+
const requestHeaders = {
|
|
3114
|
+
host,
|
|
3115
|
+
"x-amz-date": amzDate,
|
|
3116
|
+
"x-amz-content-sha256": payloadHash
|
|
3117
|
+
};
|
|
3118
|
+
if (sessionToken) {
|
|
3119
|
+
requestHeaders["x-amz-security-token"] = sessionToken;
|
|
3120
|
+
}
|
|
3121
|
+
const canonicalHeaders = Object.keys(requestHeaders).sort().map((k) => `${k.toLowerCase()}:${requestHeaders[k].trim()}
|
|
3122
|
+
`).join("");
|
|
3123
|
+
const signedHeaders = Object.keys(requestHeaders).sort().map((k) => k.toLowerCase()).join(";");
|
|
3124
|
+
const canonicalRequest = [
|
|
3125
|
+
"PUT",
|
|
3126
|
+
`/${key}`,
|
|
3127
|
+
`partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
|
|
3128
|
+
canonicalHeaders,
|
|
3129
|
+
signedHeaders,
|
|
3130
|
+
payloadHash
|
|
3131
|
+
].join(`
|
|
3132
|
+
`);
|
|
3133
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
3134
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
3135
|
+
const stringToSign = [
|
|
3136
|
+
algorithm,
|
|
3137
|
+
amzDate,
|
|
3138
|
+
credentialScope,
|
|
3139
|
+
crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
3140
|
+
].join(`
|
|
3141
|
+
`);
|
|
3142
|
+
const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
3143
|
+
const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
|
|
3144
|
+
const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
|
|
3145
|
+
const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
|
|
3146
|
+
const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3147
|
+
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
3148
|
+
const response = await fetch(url, {
|
|
3149
|
+
method: "PUT",
|
|
3150
|
+
headers: {
|
|
3151
|
+
...requestHeaders,
|
|
3152
|
+
Authorization: authHeader
|
|
3153
|
+
},
|
|
3154
|
+
body: toFetchBody(body)
|
|
3155
|
+
});
|
|
3156
|
+
if (!response.ok) {
|
|
3157
|
+
const text = await response.text();
|
|
3158
|
+
throw new Error(`Upload part failed: ${response.status} ${text}`);
|
|
3159
|
+
}
|
|
3160
|
+
return { ETag: response.headers.get("etag") || "" };
|
|
3161
|
+
}
|
|
3162
|
+
async completeMultipartUpload(bucket, key, uploadId, parts) {
|
|
3163
|
+
const partsXml = parts.sort((a, b) => a.PartNumber - b.PartNumber).map((p) => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>${p.ETag}</ETag></Part>`).join("");
|
|
3164
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3165
|
+
<CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
|
|
3166
|
+
await this.client.request({
|
|
3167
|
+
service: "s3",
|
|
3168
|
+
region: this.region,
|
|
3169
|
+
method: "POST",
|
|
3170
|
+
path: `/${bucket}/${key}`,
|
|
3171
|
+
queryParams: { uploadId },
|
|
3172
|
+
headers: { "Content-Type": "application/xml" },
|
|
3173
|
+
body
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
async abortMultipartUpload(bucket, key, uploadId) {
|
|
3177
|
+
await this.client.request({
|
|
3178
|
+
service: "s3",
|
|
3179
|
+
region: this.region,
|
|
3180
|
+
method: "DELETE",
|
|
3181
|
+
path: `/${bucket}/${key}`,
|
|
3182
|
+
queryParams: { uploadId }
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
async listMultipartUploads(bucket) {
|
|
3186
|
+
const result = await this.client.request({
|
|
3187
|
+
service: "s3",
|
|
3188
|
+
region: this.region,
|
|
3189
|
+
method: "GET",
|
|
3190
|
+
path: `/${bucket}`,
|
|
3191
|
+
queryParams: { uploads: "" }
|
|
3192
|
+
});
|
|
3193
|
+
const uploads = result?.ListMultipartUploadsResult?.Upload;
|
|
3194
|
+
if (!uploads)
|
|
3195
|
+
return [];
|
|
3196
|
+
const list = Array.isArray(uploads) ? uploads : [uploads];
|
|
3197
|
+
return list.map((u) => ({
|
|
3198
|
+
Key: u.Key,
|
|
3199
|
+
UploadId: u.UploadId,
|
|
3200
|
+
Initiated: u.Initiated
|
|
3201
|
+
}));
|
|
3202
|
+
}
|
|
3203
|
+
async restoreObject(bucket, key, days, tier = "Standard") {
|
|
3204
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3205
|
+
<RestoreRequest>
|
|
3206
|
+
<Days>${days}</Days>
|
|
3207
|
+
<GlacierJobParameters>
|
|
3208
|
+
<Tier>${tier}</Tier>
|
|
3209
|
+
</GlacierJobParameters>
|
|
3210
|
+
</RestoreRequest>`;
|
|
3211
|
+
await this.client.request({
|
|
3212
|
+
service: "s3",
|
|
3213
|
+
region: this.region,
|
|
3214
|
+
method: "POST",
|
|
3215
|
+
path: `/${bucket}/${key}`,
|
|
3216
|
+
queryParams: { restore: "" },
|
|
3217
|
+
headers: { "Content-Type": "application/xml" },
|
|
3218
|
+
body
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
async selectObjectContent(bucket, key, expression, inputFormat, outputFormat = "JSON") {
|
|
3222
|
+
let inputSerialization = "";
|
|
3223
|
+
if (inputFormat === "CSV") {
|
|
3224
|
+
inputSerialization = "<CSV><FileHeaderInfo>USE</FileHeaderInfo></CSV>";
|
|
3225
|
+
} else if (inputFormat === "JSON") {
|
|
3226
|
+
inputSerialization = "<JSON><Type>DOCUMENT</Type></JSON>";
|
|
3227
|
+
} else {
|
|
3228
|
+
inputSerialization = "<Parquet/>";
|
|
3229
|
+
}
|
|
3230
|
+
let outputSerialization = "";
|
|
3231
|
+
if (outputFormat === "CSV") {
|
|
3232
|
+
outputSerialization = "<CSV/>";
|
|
3233
|
+
} else {
|
|
3234
|
+
outputSerialization = "<JSON/>";
|
|
3235
|
+
}
|
|
3236
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3237
|
+
<SelectObjectContentRequest>
|
|
3238
|
+
<Expression>${expression}</Expression>
|
|
3239
|
+
<ExpressionType>SQL</ExpressionType>
|
|
3240
|
+
<InputSerialization>${inputSerialization}</InputSerialization>
|
|
3241
|
+
<OutputSerialization>${outputSerialization}</OutputSerialization>
|
|
3242
|
+
</SelectObjectContentRequest>`;
|
|
3243
|
+
const result = await this.client.request({
|
|
3244
|
+
service: "s3",
|
|
3245
|
+
region: this.region,
|
|
3246
|
+
method: "POST",
|
|
3247
|
+
path: `/${bucket}/${key}`,
|
|
3248
|
+
queryParams: { select: "", "select-type": "2" },
|
|
3249
|
+
headers: { "Content-Type": "application/xml" },
|
|
3250
|
+
body,
|
|
3251
|
+
rawResponse: true
|
|
3252
|
+
});
|
|
3253
|
+
return result;
|
|
3254
|
+
}
|
|
3255
|
+
async getSignedUrl(options) {
|
|
3256
|
+
const { bucket, key, expiresIn = 3600, operation = "getObject" } = options;
|
|
3257
|
+
const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
|
|
3258
|
+
const now = new Date;
|
|
3259
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
3260
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
3261
|
+
const host = this.s3VirtualHost(bucket);
|
|
3262
|
+
const method = operation === "putObject" ? "PUT" : "GET";
|
|
3263
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
3264
|
+
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
3265
|
+
const credential = `${accessKeyId}/${credentialScope}`;
|
|
3266
|
+
const queryParams = {
|
|
3267
|
+
"X-Amz-Algorithm": algorithm,
|
|
3268
|
+
"X-Amz-Credential": credential,
|
|
3269
|
+
"X-Amz-Date": amzDate,
|
|
3270
|
+
"X-Amz-Expires": expiresIn.toString(),
|
|
3271
|
+
"X-Amz-SignedHeaders": "host"
|
|
3272
|
+
};
|
|
3273
|
+
if (sessionToken) {
|
|
3274
|
+
queryParams["X-Amz-Security-Token"] = sessionToken;
|
|
3275
|
+
}
|
|
3276
|
+
const sortedParams = Object.keys(queryParams).sort();
|
|
3277
|
+
const canonicalQuerystring = sortedParams.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
3278
|
+
const canonicalUri = "/" + key;
|
|
3279
|
+
const canonicalHeaders = `host:${host}
|
|
3280
|
+
`;
|
|
3281
|
+
const signedHeaders = "host";
|
|
3282
|
+
const payloadHash = "UNSIGNED-PAYLOAD";
|
|
3283
|
+
const canonicalRequest = [
|
|
3284
|
+
method,
|
|
3285
|
+
canonicalUri,
|
|
3286
|
+
canonicalQuerystring,
|
|
3287
|
+
canonicalHeaders,
|
|
3288
|
+
signedHeaders,
|
|
3289
|
+
payloadHash
|
|
3290
|
+
].join(`
|
|
3291
|
+
`);
|
|
3292
|
+
const stringToSign = [
|
|
3293
|
+
algorithm,
|
|
3294
|
+
amzDate,
|
|
3295
|
+
credentialScope,
|
|
3296
|
+
crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
3297
|
+
].join(`
|
|
3298
|
+
`);
|
|
3299
|
+
const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
3300
|
+
const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
|
|
3301
|
+
const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
|
|
3302
|
+
const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
|
|
3303
|
+
const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3304
|
+
const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`;
|
|
3305
|
+
return presignedUrl;
|
|
3306
|
+
}
|
|
3307
|
+
async listObjects(options) {
|
|
3308
|
+
const { bucket, prefix, maxKeys = 1000, continuationToken } = options;
|
|
3309
|
+
const queryParams = {
|
|
3310
|
+
"list-type": "2",
|
|
3311
|
+
"max-keys": maxKeys.toString()
|
|
3312
|
+
};
|
|
3313
|
+
if (prefix)
|
|
3314
|
+
queryParams.prefix = prefix;
|
|
3315
|
+
if (continuationToken)
|
|
3316
|
+
queryParams["continuation-token"] = continuationToken;
|
|
3317
|
+
const result = await this.client.request({
|
|
3318
|
+
service: "s3",
|
|
3319
|
+
region: this.region,
|
|
3320
|
+
method: "GET",
|
|
3321
|
+
path: `/${bucket}`,
|
|
3322
|
+
queryParams
|
|
3323
|
+
});
|
|
3324
|
+
const objects = [];
|
|
3325
|
+
const listResult = result?.ListBucketResult ?? result;
|
|
3326
|
+
if (listResult?.Contents) {
|
|
3327
|
+
const items = Array.isArray(listResult.Contents) ? listResult.Contents : [listResult.Contents];
|
|
3328
|
+
for (const item of items) {
|
|
3329
|
+
objects.push({
|
|
3330
|
+
Key: item.Key || "",
|
|
3331
|
+
LastModified: item.LastModified || "",
|
|
3332
|
+
Size: Number.parseInt(item.Size || "0"),
|
|
3333
|
+
ETag: item.ETag
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
return {
|
|
3338
|
+
objects,
|
|
3339
|
+
nextContinuationToken: listResult?.NextContinuationToken
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
async emptyBucket(bucket) {
|
|
3343
|
+
let deletedCount = 0;
|
|
3344
|
+
const objects = await this.listAllObjects({ bucket });
|
|
3345
|
+
if (objects.length > 0) {
|
|
3346
|
+
for (let i = 0;i < objects.length; i += 1000) {
|
|
3347
|
+
const batch2 = objects.slice(i, i + 1000);
|
|
3348
|
+
const keys = batch2.map((obj) => obj.Key);
|
|
3349
|
+
await this.deleteObjects(bucket, keys);
|
|
3350
|
+
deletedCount += keys.length;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
try {
|
|
3354
|
+
let keyMarker;
|
|
3355
|
+
let versionIdMarker;
|
|
3356
|
+
do {
|
|
3357
|
+
const versionsResult = await this.listObjectVersions({
|
|
3358
|
+
bucket,
|
|
3359
|
+
keyMarker,
|
|
3360
|
+
versionIdMarker,
|
|
3361
|
+
maxKeys: 1000
|
|
3362
|
+
});
|
|
3363
|
+
const versionsToDelete = [];
|
|
3364
|
+
if (versionsResult.versions) {
|
|
3365
|
+
for (const version2 of versionsResult.versions) {
|
|
3366
|
+
versionsToDelete.push({ Key: version2.Key, VersionId: version2.VersionId });
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
if (versionsResult.deleteMarkers) {
|
|
3370
|
+
for (const marker of versionsResult.deleteMarkers) {
|
|
3371
|
+
versionsToDelete.push({ Key: marker.Key, VersionId: marker.VersionId });
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (versionsToDelete.length > 0) {
|
|
3375
|
+
await this.deleteObjectVersions(bucket, versionsToDelete);
|
|
3376
|
+
deletedCount += versionsToDelete.length;
|
|
3377
|
+
}
|
|
3378
|
+
keyMarker = versionsResult.nextKeyMarker;
|
|
3379
|
+
versionIdMarker = versionsResult.nextVersionIdMarker;
|
|
3380
|
+
} while (keyMarker);
|
|
3381
|
+
} catch {}
|
|
3382
|
+
return { deletedCount };
|
|
3383
|
+
}
|
|
3384
|
+
async listObjectVersions(options) {
|
|
3385
|
+
const { bucket, prefix, keyMarker, versionIdMarker, maxKeys = 1000 } = options;
|
|
3386
|
+
const queryParams = {
|
|
3387
|
+
versions: "",
|
|
3388
|
+
"max-keys": maxKeys.toString()
|
|
3389
|
+
};
|
|
3390
|
+
if (prefix)
|
|
3391
|
+
queryParams.prefix = prefix;
|
|
3392
|
+
if (keyMarker)
|
|
3393
|
+
queryParams["key-marker"] = keyMarker;
|
|
3394
|
+
if (versionIdMarker)
|
|
3395
|
+
queryParams["version-id-marker"] = versionIdMarker;
|
|
3396
|
+
const result = await this.client.request({
|
|
3397
|
+
service: "s3",
|
|
3398
|
+
region: this.region,
|
|
3399
|
+
method: "GET",
|
|
3400
|
+
path: `/${bucket}`,
|
|
3401
|
+
queryParams
|
|
3402
|
+
});
|
|
3403
|
+
const versions = [];
|
|
3404
|
+
const deleteMarkers = [];
|
|
3405
|
+
if (result.Version) {
|
|
3406
|
+
const versionList = Array.isArray(result.Version) ? result.Version : [result.Version];
|
|
3407
|
+
for (const v of versionList) {
|
|
3408
|
+
versions.push({
|
|
3409
|
+
Key: v.Key,
|
|
3410
|
+
VersionId: v.VersionId,
|
|
3411
|
+
IsLatest: v.IsLatest === "true"
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
if (result.DeleteMarker) {
|
|
3416
|
+
const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker];
|
|
3417
|
+
for (const m of markerList) {
|
|
3418
|
+
deleteMarkers.push({
|
|
3419
|
+
Key: m.Key,
|
|
3420
|
+
VersionId: m.VersionId,
|
|
3421
|
+
IsLatest: m.IsLatest === "true"
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
return {
|
|
3426
|
+
versions,
|
|
3427
|
+
deleteMarkers,
|
|
3428
|
+
nextKeyMarker: result.NextKeyMarker,
|
|
3429
|
+
nextVersionIdMarker: result.NextVersionIdMarker
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
async deleteObjectVersions(bucket, objects) {
|
|
3433
|
+
const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3434
|
+
<Delete>
|
|
3435
|
+
<Quiet>true</Quiet>
|
|
3436
|
+
${objects.map((obj) => `<Object><Key>${obj.Key}</Key>${obj.VersionId ? `<VersionId>${obj.VersionId}</VersionId>` : ""}</Object>`).join(`
|
|
3437
|
+
`)}
|
|
3438
|
+
</Delete>`;
|
|
3439
|
+
const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
|
|
3440
|
+
await this.client.request({
|
|
3441
|
+
service: "s3",
|
|
3442
|
+
region: this.region,
|
|
3443
|
+
method: "POST",
|
|
3444
|
+
path: `/${bucket}`,
|
|
3445
|
+
queryParams: { delete: "" },
|
|
3446
|
+
body: deleteXml,
|
|
3447
|
+
headers: {
|
|
3448
|
+
"Content-Type": "application/xml",
|
|
3449
|
+
"Content-MD5": contentMd5
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
var init_s3 = __esm(() => {
|
|
3455
|
+
init_client();
|
|
3456
|
+
});
|
|
3457
|
+
|
|
3458
|
+
// src/aws/cloudformation.ts
|
|
3459
|
+
class CloudFormationClient {
|
|
3460
|
+
client;
|
|
3461
|
+
region;
|
|
3462
|
+
constructor(region = "us-east-1", profile) {
|
|
3463
|
+
this.region = region;
|
|
3464
|
+
this.client = new AWSClient;
|
|
3465
|
+
}
|
|
3466
|
+
async createStack(options) {
|
|
3467
|
+
const params = {
|
|
3468
|
+
Action: "CreateStack",
|
|
3469
|
+
StackName: options.stackName,
|
|
3470
|
+
Version: "2010-05-15"
|
|
3471
|
+
};
|
|
3472
|
+
if (options.templateBody) {
|
|
3473
|
+
params.TemplateBody = options.templateBody;
|
|
3474
|
+
} else if (options.templateUrl) {
|
|
3475
|
+
params.TemplateURL = options.templateUrl;
|
|
3476
|
+
} else {
|
|
3477
|
+
throw new Error("Either templateBody or templateUrl must be provided");
|
|
3478
|
+
}
|
|
3479
|
+
if (options.parameters) {
|
|
3480
|
+
options.parameters.forEach((param, index) => {
|
|
3481
|
+
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
|
|
3482
|
+
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
if (options.capabilities) {
|
|
3486
|
+
options.capabilities.forEach((cap, index) => {
|
|
3487
|
+
params[`Capabilities.member.${index + 1}`] = cap;
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
if (options.roleArn) {
|
|
3491
|
+
params.RoleARN = options.roleArn;
|
|
3492
|
+
}
|
|
3493
|
+
if (options.tags) {
|
|
3494
|
+
options.tags.forEach((tag, index) => {
|
|
3495
|
+
params[`Tags.member.${index + 1}.Key`] = tag.Key;
|
|
3496
|
+
params[`Tags.member.${index + 1}.Value`] = tag.Value;
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
if (options.timeoutInMinutes) {
|
|
3500
|
+
params.TimeoutInMinutes = options.timeoutInMinutes;
|
|
3501
|
+
}
|
|
3502
|
+
if (options.onFailure) {
|
|
3503
|
+
params.OnFailure = options.onFailure;
|
|
3656
3504
|
}
|
|
3657
3505
|
const result = await this.client.request({
|
|
3658
|
-
service: "
|
|
3506
|
+
service: "cloudformation",
|
|
3659
3507
|
region: this.region,
|
|
3660
3508
|
method: "POST",
|
|
3661
|
-
path:
|
|
3662
|
-
|
|
3663
|
-
headers
|
|
3509
|
+
path: "/",
|
|
3510
|
+
body: new URLSearchParams(params).toString()
|
|
3664
3511
|
});
|
|
3665
|
-
return {
|
|
3512
|
+
return { StackId: result.StackId || result.CreateStackResult?.StackId };
|
|
3666
3513
|
}
|
|
3667
|
-
async
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
3673
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
3674
|
-
const payloadHash = crypto3.createHash("sha256").update(body).digest("hex");
|
|
3675
|
-
const requestHeaders = {
|
|
3676
|
-
host,
|
|
3677
|
-
"x-amz-date": amzDate,
|
|
3678
|
-
"x-amz-content-sha256": payloadHash
|
|
3514
|
+
async updateStack(options) {
|
|
3515
|
+
const params = {
|
|
3516
|
+
Action: "UpdateStack",
|
|
3517
|
+
StackName: options.stackName,
|
|
3518
|
+
Version: "2010-05-15"
|
|
3679
3519
|
};
|
|
3680
|
-
if (
|
|
3681
|
-
|
|
3520
|
+
if (options.templateBody) {
|
|
3521
|
+
params.TemplateBody = options.templateBody;
|
|
3522
|
+
} else if (options.templateUrl) {
|
|
3523
|
+
params.TemplateURL = options.templateUrl;
|
|
3682
3524
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
payloadHash
|
|
3693
|
-
].join(`
|
|
3694
|
-
`);
|
|
3695
|
-
const algorithm = "AWS4-HMAC-SHA256";
|
|
3696
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
3697
|
-
const stringToSign = [
|
|
3698
|
-
algorithm,
|
|
3699
|
-
amzDate,
|
|
3700
|
-
credentialScope,
|
|
3701
|
-
crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
|
|
3702
|
-
].join(`
|
|
3703
|
-
`);
|
|
3704
|
-
const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
|
|
3705
|
-
const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
|
|
3706
|
-
const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
|
|
3707
|
-
const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
|
|
3708
|
-
const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
|
|
3709
|
-
const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
3710
|
-
const response = await fetch(url, {
|
|
3711
|
-
method: "PUT",
|
|
3712
|
-
headers: {
|
|
3713
|
-
...requestHeaders,
|
|
3714
|
-
Authorization: authHeader
|
|
3715
|
-
},
|
|
3716
|
-
body: toFetchBody(body)
|
|
3717
|
-
});
|
|
3718
|
-
if (!response.ok) {
|
|
3719
|
-
const text = await response.text();
|
|
3720
|
-
throw new Error(`Upload part failed: ${response.status} ${text}`);
|
|
3525
|
+
if (options.parameters) {
|
|
3526
|
+
options.parameters.forEach((param, index) => {
|
|
3527
|
+
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
|
|
3528
|
+
if (param.UsePreviousValue) {
|
|
3529
|
+
params[`Parameters.member.${index + 1}.UsePreviousValue`] = "true";
|
|
3530
|
+
} else {
|
|
3531
|
+
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
|
|
3532
|
+
}
|
|
3533
|
+
});
|
|
3721
3534
|
}
|
|
3722
|
-
|
|
3535
|
+
if (options.capabilities) {
|
|
3536
|
+
options.capabilities.forEach((cap, index) => {
|
|
3537
|
+
params[`Capabilities.member.${index + 1}`] = cap;
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
if (options.roleArn) {
|
|
3541
|
+
params.RoleARN = options.roleArn;
|
|
3542
|
+
}
|
|
3543
|
+
if (options.tags) {
|
|
3544
|
+
options.tags.forEach((tag, index) => {
|
|
3545
|
+
params[`Tags.member.${index + 1}.Key`] = tag.Key;
|
|
3546
|
+
params[`Tags.member.${index + 1}.Value`] = tag.Value;
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
const result = await this.client.request({
|
|
3550
|
+
service: "cloudformation",
|
|
3551
|
+
region: this.region,
|
|
3552
|
+
method: "POST",
|
|
3553
|
+
path: "/",
|
|
3554
|
+
body: new URLSearchParams(params).toString()
|
|
3555
|
+
});
|
|
3556
|
+
return { StackId: result.StackId || result.UpdateStackResult?.StackId };
|
|
3723
3557
|
}
|
|
3724
|
-
async
|
|
3725
|
-
const
|
|
3726
|
-
|
|
3727
|
-
|
|
3558
|
+
async deleteStack(stackName, roleArn, retainResources) {
|
|
3559
|
+
const params = {
|
|
3560
|
+
Action: "DeleteStack",
|
|
3561
|
+
StackName: stackName,
|
|
3562
|
+
Version: "2010-05-15"
|
|
3563
|
+
};
|
|
3564
|
+
if (roleArn) {
|
|
3565
|
+
params.RoleARN = roleArn;
|
|
3566
|
+
}
|
|
3567
|
+
if (retainResources && retainResources.length > 0) {
|
|
3568
|
+
retainResources.forEach((resource, index) => {
|
|
3569
|
+
params[`RetainResources.member.${index + 1}`] = resource;
|
|
3570
|
+
});
|
|
3571
|
+
}
|
|
3728
3572
|
await this.client.request({
|
|
3729
|
-
service: "
|
|
3573
|
+
service: "cloudformation",
|
|
3730
3574
|
region: this.region,
|
|
3731
3575
|
method: "POST",
|
|
3732
|
-
path:
|
|
3733
|
-
|
|
3734
|
-
headers: { "Content-Type": "application/xml" },
|
|
3735
|
-
body
|
|
3576
|
+
path: "/",
|
|
3577
|
+
body: new URLSearchParams(params).toString()
|
|
3736
3578
|
});
|
|
3737
3579
|
}
|
|
3738
|
-
async
|
|
3739
|
-
|
|
3740
|
-
|
|
3580
|
+
async describeStacks(options = {}) {
|
|
3581
|
+
const params = {
|
|
3582
|
+
Action: "DescribeStacks",
|
|
3583
|
+
Version: "2010-05-15"
|
|
3584
|
+
};
|
|
3585
|
+
if (options.stackName) {
|
|
3586
|
+
params.StackName = options.stackName;
|
|
3587
|
+
}
|
|
3588
|
+
const result = await this.client.request({
|
|
3589
|
+
service: "cloudformation",
|
|
3741
3590
|
region: this.region,
|
|
3742
|
-
method: "
|
|
3743
|
-
path:
|
|
3744
|
-
|
|
3591
|
+
method: "POST",
|
|
3592
|
+
path: "/",
|
|
3593
|
+
body: new URLSearchParams(params).toString()
|
|
3745
3594
|
});
|
|
3595
|
+
const stacks = this.parseStacksResponse(result);
|
|
3596
|
+
return { Stacks: stacks };
|
|
3746
3597
|
}
|
|
3747
|
-
async
|
|
3598
|
+
async describeStackEvents(stackName) {
|
|
3599
|
+
const params = {
|
|
3600
|
+
Action: "DescribeStackEvents",
|
|
3601
|
+
StackName: stackName,
|
|
3602
|
+
Version: "2010-05-15"
|
|
3603
|
+
};
|
|
3748
3604
|
const result = await this.client.request({
|
|
3749
|
-
service: "
|
|
3605
|
+
service: "cloudformation",
|
|
3750
3606
|
region: this.region,
|
|
3751
|
-
method: "
|
|
3752
|
-
path:
|
|
3753
|
-
|
|
3607
|
+
method: "POST",
|
|
3608
|
+
path: "/",
|
|
3609
|
+
body: new URLSearchParams(params).toString()
|
|
3754
3610
|
});
|
|
3755
|
-
|
|
3756
|
-
if (!uploads)
|
|
3757
|
-
return [];
|
|
3758
|
-
const list = Array.isArray(uploads) ? uploads : [uploads];
|
|
3759
|
-
return list.map((u) => ({
|
|
3760
|
-
Key: u.Key,
|
|
3761
|
-
UploadId: u.UploadId,
|
|
3762
|
-
Initiated: u.Initiated
|
|
3763
|
-
}));
|
|
3611
|
+
return { StackEvents: this.parseStackEvents(result) };
|
|
3764
3612
|
}
|
|
3765
|
-
async
|
|
3766
|
-
const
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
await this.client.request({
|
|
3774
|
-
service: "s3",
|
|
3613
|
+
async listStackResources(stackName) {
|
|
3614
|
+
const params = {
|
|
3615
|
+
Action: "ListStackResources",
|
|
3616
|
+
StackName: stackName,
|
|
3617
|
+
Version: "2010-05-15"
|
|
3618
|
+
};
|
|
3619
|
+
const result = await this.client.request({
|
|
3620
|
+
service: "cloudformation",
|
|
3775
3621
|
region: this.region,
|
|
3776
3622
|
method: "POST",
|
|
3777
|
-
path:
|
|
3778
|
-
|
|
3779
|
-
headers: { "Content-Type": "application/xml" },
|
|
3780
|
-
body
|
|
3623
|
+
path: "/",
|
|
3624
|
+
body: new URLSearchParams(params).toString()
|
|
3781
3625
|
});
|
|
3626
|
+
const member = result?.ListStackResourcesResult?.StackResourceSummaries?.member;
|
|
3627
|
+
let resources = [];
|
|
3628
|
+
if (member) {
|
|
3629
|
+
resources = Array.isArray(member) ? member : [member];
|
|
3630
|
+
}
|
|
3631
|
+
return { StackResourceSummaries: resources };
|
|
3782
3632
|
}
|
|
3783
|
-
async
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3633
|
+
async waitForStack(stackName, waitType) {
|
|
3634
|
+
const targetStatuses = {
|
|
3635
|
+
"stack-create-complete": ["CREATE_COMPLETE"],
|
|
3636
|
+
"stack-update-complete": ["UPDATE_COMPLETE"],
|
|
3637
|
+
"stack-delete-complete": ["DELETE_COMPLETE"]
|
|
3638
|
+
};
|
|
3639
|
+
const failureStatuses = [
|
|
3640
|
+
"CREATE_FAILED",
|
|
3641
|
+
"ROLLBACK_FAILED",
|
|
3642
|
+
"ROLLBACK_COMPLETE",
|
|
3643
|
+
"UPDATE_ROLLBACK_FAILED",
|
|
3644
|
+
"UPDATE_ROLLBACK_COMPLETE"
|
|
3645
|
+
];
|
|
3646
|
+
const targets = targetStatuses[waitType];
|
|
3647
|
+
const maxAttempts = 360;
|
|
3648
|
+
let attempts = 0;
|
|
3649
|
+
while (attempts < maxAttempts) {
|
|
3650
|
+
try {
|
|
3651
|
+
const result = await this.describeStacks({ stackName });
|
|
3652
|
+
if (result.Stacks.length === 0) {
|
|
3653
|
+
if (waitType === "stack-delete-complete") {
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
if (attempts % 10 === 0) {
|
|
3657
|
+
console.log(`[waitForStack] Attempt ${attempts}: Stack not visible yet`);
|
|
3658
|
+
}
|
|
3659
|
+
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
3660
|
+
attempts++;
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
const stack = result.Stacks[0];
|
|
3664
|
+
if (attempts % 10 === 0) {
|
|
3665
|
+
console.log(`[waitForStack] Attempt ${attempts}: Status = ${stack.StackStatus}${stack.StackStatusReason ? ` (${stack.StackStatusReason})` : ""}`);
|
|
3666
|
+
}
|
|
3667
|
+
if (targets.includes(stack.StackStatus)) {
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
if ((waitType === "stack-create-complete" || waitType === "stack-update-complete") && (stack.StackStatus === "DELETE_IN_PROGRESS" || stack.StackStatus === "DELETE_COMPLETE")) {
|
|
3671
|
+
console.log(`[waitForStack] Stack is being deleted (creation/update failed)`);
|
|
3672
|
+
let failedEventReason = "";
|
|
3673
|
+
try {
|
|
3674
|
+
const eventsResult = await this.describeStackEvents(stackName);
|
|
3675
|
+
console.log("[waitForStack] Stack events (most recent first):");
|
|
3676
|
+
for (const event of eventsResult.StackEvents.slice(0, 15)) {
|
|
3677
|
+
if (event.ResourceStatus.includes("FAILED") || event.ResourceStatusReason) {
|
|
3678
|
+
console.log(` ${event.LogicalResourceId}: ${event.ResourceStatus} - ${event.ResourceStatusReason || "No reason provided"}`);
|
|
3679
|
+
if (event.ResourceStatus.includes("FAILED") && event.ResourceStatusReason && !failedEventReason) {
|
|
3680
|
+
failedEventReason = event.ResourceStatusReason;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
} catch {}
|
|
3685
|
+
const errorReason = failedEventReason || stack.StackStatusReason || "Check CloudFormation console for details.";
|
|
3686
|
+
throw new Error(`Stack creation/update failed - stack is being deleted. Reason: ${errorReason}`);
|
|
3687
|
+
}
|
|
3688
|
+
if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
|
|
3689
|
+
const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
|
|
3690
|
+
error.code = "DELETE_FAILED";
|
|
3691
|
+
error.stackStatus = stack.StackStatus;
|
|
3692
|
+
error.statusReason = stack.StackStatusReason;
|
|
3693
|
+
throw error;
|
|
3694
|
+
}
|
|
3695
|
+
if (failureStatuses.includes(stack.StackStatus)) {
|
|
3696
|
+
throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
|
|
3697
|
+
}
|
|
3698
|
+
await new Promise((resolve13) => setTimeout(resolve13, 5000));
|
|
3699
|
+
attempts++;
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
|
|
3705
|
+
if (attempts % 10 === 0) {
|
|
3706
|
+
console.log(`[waitForStack] Attempt ${attempts}: Stack does not exist (error), retrying...`);
|
|
3707
|
+
}
|
|
3708
|
+
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
3709
|
+
attempts++;
|
|
3710
|
+
continue;
|
|
3711
|
+
}
|
|
3712
|
+
console.log(`[waitForStack] Unexpected error: ${error.message}`);
|
|
3713
|
+
throw error;
|
|
3714
|
+
}
|
|
3791
3715
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3716
|
+
console.log(`[waitForStack] Timeout after ${attempts} attempts`);
|
|
3717
|
+
throw new Error(`Timeout waiting for stack to reach ${waitType}`);
|
|
3718
|
+
}
|
|
3719
|
+
async validateTemplate(templateBody) {
|
|
3720
|
+
const params = {
|
|
3721
|
+
Action: "ValidateTemplate",
|
|
3722
|
+
TemplateBody: templateBody,
|
|
3723
|
+
Version: "2010-05-15"
|
|
3724
|
+
};
|
|
3725
|
+
return await this.client.request({
|
|
3726
|
+
service: "cloudformation",
|
|
3727
|
+
region: this.region,
|
|
3728
|
+
method: "POST",
|
|
3729
|
+
path: "/",
|
|
3730
|
+
body: new URLSearchParams(params).toString()
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
async listStacks(statusFilter) {
|
|
3734
|
+
const params = {
|
|
3735
|
+
Action: "ListStacks",
|
|
3736
|
+
Version: "2010-05-15"
|
|
3737
|
+
};
|
|
3738
|
+
if (statusFilter) {
|
|
3739
|
+
statusFilter.forEach((status, index) => {
|
|
3740
|
+
params[`StackStatusFilter.member.${index + 1}`] = status;
|
|
3741
|
+
});
|
|
3797
3742
|
}
|
|
3798
|
-
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3799
|
-
<SelectObjectContentRequest>
|
|
3800
|
-
<Expression>${expression}</Expression>
|
|
3801
|
-
<ExpressionType>SQL</ExpressionType>
|
|
3802
|
-
<InputSerialization>${inputSerialization}</InputSerialization>
|
|
3803
|
-
<OutputSerialization>${outputSerialization}</OutputSerialization>
|
|
3804
|
-
</SelectObjectContentRequest>`;
|
|
3805
3743
|
const result = await this.client.request({
|
|
3806
|
-
service: "
|
|
3744
|
+
service: "cloudformation",
|
|
3807
3745
|
region: this.region,
|
|
3808
3746
|
method: "POST",
|
|
3809
|
-
path:
|
|
3810
|
-
|
|
3811
|
-
headers: { "Content-Type": "application/xml" },
|
|
3812
|
-
body,
|
|
3813
|
-
rawResponse: true
|
|
3747
|
+
path: "/",
|
|
3748
|
+
body: new URLSearchParams(params).toString()
|
|
3814
3749
|
});
|
|
3815
|
-
|
|
3750
|
+
const response = result.ListStacksResponse || result;
|
|
3751
|
+
const summariesResult = response.ListStacksResult || response;
|
|
3752
|
+
const members = summariesResult.StackSummaries?.member || summariesResult.StackSummaries || [];
|
|
3753
|
+
const items = Array.isArray(members) ? members : members ? [members] : [];
|
|
3754
|
+
return {
|
|
3755
|
+
StackSummaries: items.map((s) => ({
|
|
3756
|
+
StackId: s.StackId || "",
|
|
3757
|
+
StackName: s.StackName || "",
|
|
3758
|
+
TemplateDescription: s.TemplateDescription,
|
|
3759
|
+
CreationTime: s.CreationTime || "",
|
|
3760
|
+
LastUpdatedTime: s.LastUpdatedTime,
|
|
3761
|
+
DeletionTime: s.DeletionTime,
|
|
3762
|
+
StackStatus: s.StackStatus || ""
|
|
3763
|
+
}))
|
|
3764
|
+
};
|
|
3816
3765
|
}
|
|
3817
|
-
async
|
|
3818
|
-
const
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
const host = `${bucket}.s3.${this.region}.amazonaws.com`;
|
|
3824
|
-
const method = operation === "putObject" ? "PUT" : "GET";
|
|
3825
|
-
const algorithm = "AWS4-HMAC-SHA256";
|
|
3826
|
-
const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
|
|
3827
|
-
const credential = `${accessKeyId}/${credentialScope}`;
|
|
3828
|
-
const queryParams = {
|
|
3829
|
-
"X-Amz-Algorithm": algorithm,
|
|
3830
|
-
"X-Amz-Credential": credential,
|
|
3831
|
-
"X-Amz-Date": amzDate,
|
|
3832
|
-
"X-Amz-Expires": expiresIn.toString(),
|
|
3833
|
-
"X-Amz-SignedHeaders": "host"
|
|
3766
|
+
async createChangeSet(options) {
|
|
3767
|
+
const params = {
|
|
3768
|
+
Action: "CreateChangeSet",
|
|
3769
|
+
StackName: options.stackName,
|
|
3770
|
+
ChangeSetName: options.changeSetName,
|
|
3771
|
+
Version: "2010-05-15"
|
|
3834
3772
|
};
|
|
3835
|
-
if (
|
|
3836
|
-
|
|
3773
|
+
if (options.templateBody) {
|
|
3774
|
+
params.TemplateBody = options.templateBody;
|
|
3775
|
+
} else if (options.templateUrl) {
|
|
3776
|
+
params.TemplateURL = options.templateUrl;
|
|
3837
3777
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
const
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3778
|
+
if (options.parameters) {
|
|
3779
|
+
options.parameters.forEach((param, index) => {
|
|
3780
|
+
params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
|
|
3781
|
+
params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
if (options.capabilities) {
|
|
3785
|
+
options.capabilities.forEach((cap, index) => {
|
|
3786
|
+
params[`Capabilities.member.${index + 1}`] = cap;
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
if (options.changeSetType) {
|
|
3790
|
+
params.ChangeSetType = options.changeSetType;
|
|
3791
|
+
}
|
|
3792
|
+
const result = await this.client.request({
|
|
3793
|
+
service: "cloudformation",
|
|
3794
|
+
region: this.region,
|
|
3795
|
+
method: "POST",
|
|
3796
|
+
path: "/",
|
|
3797
|
+
body: new URLSearchParams(params).toString()
|
|
3798
|
+
});
|
|
3799
|
+
return { Id: result.Id, StackId: result.StackId };
|
|
3800
|
+
}
|
|
3801
|
+
async describeChangeSet(stackName, changeSetName) {
|
|
3802
|
+
const params = {
|
|
3803
|
+
Action: "DescribeChangeSet",
|
|
3804
|
+
StackName: stackName,
|
|
3805
|
+
ChangeSetName: changeSetName,
|
|
3806
|
+
Version: "2010-05-15"
|
|
3807
|
+
};
|
|
3808
|
+
return await this.client.request({
|
|
3809
|
+
service: "cloudformation",
|
|
3810
|
+
region: this.region,
|
|
3811
|
+
method: "POST",
|
|
3812
|
+
path: "/",
|
|
3813
|
+
body: new URLSearchParams(params).toString()
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
async executeChangeSet(stackName, changeSetName) {
|
|
3817
|
+
const params = {
|
|
3818
|
+
Action: "ExecuteChangeSet",
|
|
3819
|
+
StackName: stackName,
|
|
3820
|
+
ChangeSetName: changeSetName,
|
|
3821
|
+
Version: "2010-05-15"
|
|
3822
|
+
};
|
|
3823
|
+
await this.client.request({
|
|
3824
|
+
service: "cloudformation",
|
|
3825
|
+
region: this.region,
|
|
3826
|
+
method: "POST",
|
|
3827
|
+
path: "/",
|
|
3828
|
+
body: new URLSearchParams(params).toString()
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
async deleteChangeSet(stackName, changeSetName) {
|
|
3832
|
+
const params = {
|
|
3833
|
+
Action: "DeleteChangeSet",
|
|
3834
|
+
StackName: stackName,
|
|
3835
|
+
ChangeSetName: changeSetName,
|
|
3836
|
+
Version: "2010-05-15"
|
|
3837
|
+
};
|
|
3838
|
+
await this.client.request({
|
|
3839
|
+
service: "cloudformation",
|
|
3840
|
+
region: this.region,
|
|
3841
|
+
method: "POST",
|
|
3842
|
+
path: "/",
|
|
3843
|
+
body: new URLSearchParams(params).toString()
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
async getStackOutputs(stackName) {
|
|
3847
|
+
const result = await this.describeStacks({ stackName });
|
|
3848
|
+
if (!result.Stacks || result.Stacks.length === 0) {
|
|
3849
|
+
throw new Error(`Stack ${stackName} not found`);
|
|
3850
|
+
}
|
|
3851
|
+
const stack = result.Stacks[0];
|
|
3852
|
+
const outputs = {};
|
|
3853
|
+
if (stack.Outputs) {
|
|
3854
|
+
for (const output of stack.Outputs) {
|
|
3855
|
+
outputs[output.OutputKey] = output.OutputValue;
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
return outputs;
|
|
3868
3859
|
}
|
|
3869
|
-
async
|
|
3870
|
-
const
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
"
|
|
3860
|
+
async getTemplate(stackName) {
|
|
3861
|
+
const params = {
|
|
3862
|
+
Action: "GetTemplate",
|
|
3863
|
+
StackName: stackName,
|
|
3864
|
+
Version: "2010-05-15"
|
|
3874
3865
|
};
|
|
3875
|
-
if (prefix)
|
|
3876
|
-
queryParams.prefix = prefix;
|
|
3877
|
-
if (continuationToken)
|
|
3878
|
-
queryParams["continuation-token"] = continuationToken;
|
|
3879
3866
|
const result = await this.client.request({
|
|
3880
|
-
service: "
|
|
3867
|
+
service: "cloudformation",
|
|
3881
3868
|
region: this.region,
|
|
3882
|
-
method: "
|
|
3883
|
-
path:
|
|
3884
|
-
|
|
3869
|
+
method: "POST",
|
|
3870
|
+
path: "/",
|
|
3871
|
+
body: new URLSearchParams(params).toString()
|
|
3885
3872
|
});
|
|
3886
|
-
const
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3873
|
+
const templateBody = result?.GetTemplateResult?.TemplateBody || result?.TemplateBody || "";
|
|
3874
|
+
return { TemplateBody: templateBody };
|
|
3875
|
+
}
|
|
3876
|
+
parseStacksResponse(result) {
|
|
3877
|
+
const stacks = [];
|
|
3878
|
+
let stackData = result?.DescribeStacksResult?.Stacks?.member || result?.Stacks?.member || result?.Stacks || result;
|
|
3879
|
+
if (stackData && !Array.isArray(stackData)) {
|
|
3880
|
+
stackData = [stackData];
|
|
3881
|
+
}
|
|
3882
|
+
if (Array.isArray(stackData)) {
|
|
3883
|
+
for (const s of stackData) {
|
|
3884
|
+
if (s.StackId || s.StackName) {
|
|
3885
|
+
let outputs;
|
|
3886
|
+
if (s.Outputs?.member) {
|
|
3887
|
+
const outputData = Array.isArray(s.Outputs.member) ? s.Outputs.member : [s.Outputs.member];
|
|
3888
|
+
outputs = outputData.map((o) => ({
|
|
3889
|
+
OutputKey: o.OutputKey,
|
|
3890
|
+
OutputValue: o.OutputValue,
|
|
3891
|
+
Description: o.Description,
|
|
3892
|
+
ExportName: o.ExportName
|
|
3893
|
+
}));
|
|
3894
|
+
}
|
|
3895
|
+
stacks.push({
|
|
3896
|
+
StackId: s.StackId,
|
|
3897
|
+
StackName: s.StackName,
|
|
3898
|
+
StackStatus: s.StackStatus,
|
|
3899
|
+
CreationTime: s.CreationTime,
|
|
3900
|
+
LastUpdatedTime: s.LastUpdatedTime,
|
|
3901
|
+
StackStatusReason: s.StackStatusReason,
|
|
3902
|
+
Outputs: outputs
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3897
3905
|
}
|
|
3906
|
+
} else if (result.StackId) {
|
|
3907
|
+
stacks.push({
|
|
3908
|
+
StackId: result.StackId,
|
|
3909
|
+
StackName: result.StackName,
|
|
3910
|
+
StackStatus: result.StackStatus,
|
|
3911
|
+
CreationTime: result.CreationTime,
|
|
3912
|
+
LastUpdatedTime: result.LastUpdatedTime
|
|
3913
|
+
});
|
|
3898
3914
|
}
|
|
3899
|
-
return
|
|
3900
|
-
objects,
|
|
3901
|
-
nextContinuationToken: listResult?.NextContinuationToken
|
|
3902
|
-
};
|
|
3915
|
+
return stacks;
|
|
3903
3916
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
if (
|
|
3908
|
-
|
|
3909
|
-
const batch2 = objects.slice(i, i + 1000);
|
|
3910
|
-
const keys = batch2.map((obj) => obj.Key);
|
|
3911
|
-
await this.deleteObjects(bucket, keys);
|
|
3912
|
-
deletedCount += keys.length;
|
|
3913
|
-
}
|
|
3917
|
+
parseStackEvents(result) {
|
|
3918
|
+
const events = [];
|
|
3919
|
+
let eventData = result?.DescribeStackEventsResult?.StackEvents?.member || result?.StackEvents?.member || result?.StackEvents || [];
|
|
3920
|
+
if (eventData && !Array.isArray(eventData)) {
|
|
3921
|
+
eventData = [eventData];
|
|
3914
3922
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
maxKeys: 1000
|
|
3923
|
+
for (const e of eventData) {
|
|
3924
|
+
if (e.LogicalResourceId) {
|
|
3925
|
+
events.push({
|
|
3926
|
+
Timestamp: e.Timestamp,
|
|
3927
|
+
ResourceType: e.ResourceType,
|
|
3928
|
+
LogicalResourceId: e.LogicalResourceId,
|
|
3929
|
+
ResourceStatus: e.ResourceStatus,
|
|
3930
|
+
ResourceStatusReason: e.ResourceStatusReason
|
|
3924
3931
|
});
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
return events;
|
|
3935
|
+
}
|
|
3936
|
+
async waitForStackWithProgress(stackName, waitType, onProgress) {
|
|
3937
|
+
const targetStatuses = {
|
|
3938
|
+
"stack-create-complete": ["CREATE_COMPLETE"],
|
|
3939
|
+
"stack-update-complete": ["UPDATE_COMPLETE"],
|
|
3940
|
+
"stack-delete-complete": ["DELETE_COMPLETE"]
|
|
3941
|
+
};
|
|
3942
|
+
const failureStatuses = [
|
|
3943
|
+
"CREATE_FAILED",
|
|
3944
|
+
"ROLLBACK_FAILED",
|
|
3945
|
+
"ROLLBACK_COMPLETE",
|
|
3946
|
+
"UPDATE_ROLLBACK_FAILED",
|
|
3947
|
+
"UPDATE_ROLLBACK_COMPLETE"
|
|
3948
|
+
];
|
|
3949
|
+
const targets = targetStatuses[waitType];
|
|
3950
|
+
const maxAttempts = 360;
|
|
3951
|
+
let attempts = 0;
|
|
3952
|
+
const seenEvents = new Set;
|
|
3953
|
+
while (attempts < maxAttempts) {
|
|
3954
|
+
try {
|
|
3955
|
+
if (onProgress) {
|
|
3956
|
+
try {
|
|
3957
|
+
const eventsResult = await this.describeStackEvents(stackName);
|
|
3958
|
+
const events = [...eventsResult.StackEvents || []].reverse();
|
|
3959
|
+
for (const event of events) {
|
|
3960
|
+
const eventKey = `${event.LogicalResourceId}-${event.ResourceStatus}-${event.Timestamp}`;
|
|
3961
|
+
if (!seenEvents.has(eventKey)) {
|
|
3962
|
+
seenEvents.add(eventKey);
|
|
3963
|
+
onProgress({
|
|
3964
|
+
resourceId: event.LogicalResourceId,
|
|
3965
|
+
resourceType: event.ResourceType,
|
|
3966
|
+
status: event.ResourceStatus,
|
|
3967
|
+
reason: event.ResourceStatusReason,
|
|
3968
|
+
timestamp: event.Timestamp
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
} catch {}
|
|
3930
3973
|
}
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3974
|
+
const result = await this.describeStacks({ stackName });
|
|
3975
|
+
if (result.Stacks.length === 0) {
|
|
3976
|
+
if (waitType === "stack-delete-complete") {
|
|
3977
|
+
return;
|
|
3934
3978
|
}
|
|
3979
|
+
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
3980
|
+
attempts++;
|
|
3981
|
+
continue;
|
|
3935
3982
|
}
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3983
|
+
const stack = result.Stacks[0];
|
|
3984
|
+
if (targets.includes(stack.StackStatus)) {
|
|
3985
|
+
return;
|
|
3939
3986
|
}
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
path: `/${bucket}`,
|
|
3963
|
-
queryParams
|
|
3964
|
-
});
|
|
3965
|
-
const versions = [];
|
|
3966
|
-
const deleteMarkers = [];
|
|
3967
|
-
if (result.Version) {
|
|
3968
|
-
const versionList = Array.isArray(result.Version) ? result.Version : [result.Version];
|
|
3969
|
-
for (const v of versionList) {
|
|
3970
|
-
versions.push({
|
|
3971
|
-
Key: v.Key,
|
|
3972
|
-
VersionId: v.VersionId,
|
|
3973
|
-
IsLatest: v.IsLatest === "true"
|
|
3974
|
-
});
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
if (result.DeleteMarker) {
|
|
3978
|
-
const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker];
|
|
3979
|
-
for (const m of markerList) {
|
|
3980
|
-
deleteMarkers.push({
|
|
3981
|
-
Key: m.Key,
|
|
3982
|
-
VersionId: m.VersionId,
|
|
3983
|
-
IsLatest: m.IsLatest === "true"
|
|
3984
|
-
});
|
|
3987
|
+
if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
|
|
3988
|
+
const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
|
|
3989
|
+
error.code = "DELETE_FAILED";
|
|
3990
|
+
error.stackStatus = stack.StackStatus;
|
|
3991
|
+
error.statusReason = stack.StackStatusReason;
|
|
3992
|
+
throw error;
|
|
3993
|
+
}
|
|
3994
|
+
if (failureStatuses.includes(stack.StackStatus)) {
|
|
3995
|
+
throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
|
|
3996
|
+
}
|
|
3997
|
+
await new Promise((resolve13) => setTimeout(resolve13, 3000));
|
|
3998
|
+
attempts++;
|
|
3999
|
+
} catch (error) {
|
|
4000
|
+
if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
|
|
4004
|
+
await new Promise((resolve13) => setTimeout(resolve13, 2000));
|
|
4005
|
+
attempts++;
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
throw error;
|
|
3985
4009
|
}
|
|
3986
4010
|
}
|
|
3987
|
-
|
|
3988
|
-
versions,
|
|
3989
|
-
deleteMarkers,
|
|
3990
|
-
nextKeyMarker: result.NextKeyMarker,
|
|
3991
|
-
nextVersionIdMarker: result.NextVersionIdMarker
|
|
3992
|
-
};
|
|
4011
|
+
throw new Error(`Timeout waiting for stack to reach ${waitType}`);
|
|
3993
4012
|
}
|
|
3994
|
-
async
|
|
3995
|
-
const
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4013
|
+
async waitForStackComplete(stackName, maxAttempts = 120, delayMs = 5000) {
|
|
4014
|
+
const successStatuses = [
|
|
4015
|
+
"CREATE_COMPLETE",
|
|
4016
|
+
"UPDATE_COMPLETE",
|
|
4017
|
+
"DELETE_COMPLETE"
|
|
4018
|
+
];
|
|
4019
|
+
const failureStatuses = [
|
|
4020
|
+
"CREATE_FAILED",
|
|
4021
|
+
"UPDATE_FAILED",
|
|
4022
|
+
"DELETE_FAILED",
|
|
4023
|
+
"ROLLBACK_COMPLETE",
|
|
4024
|
+
"ROLLBACK_FAILED",
|
|
4025
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
4026
|
+
"UPDATE_ROLLBACK_FAILED"
|
|
4027
|
+
];
|
|
4028
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
4029
|
+
try {
|
|
4030
|
+
const result = await this.describeStacks({ stackName });
|
|
4031
|
+
if (result.Stacks.length === 0) {
|
|
4032
|
+
return { success: true, status: "DELETE_COMPLETE" };
|
|
4033
|
+
}
|
|
4034
|
+
const stack = result.Stacks[0];
|
|
4035
|
+
const status = stack.StackStatus;
|
|
4036
|
+
if (successStatuses.includes(status)) {
|
|
4037
|
+
return { success: true, status };
|
|
4038
|
+
}
|
|
4039
|
+
if (failureStatuses.includes(status)) {
|
|
4040
|
+
return { success: false, status, reason: stack.StackStatusReason };
|
|
4041
|
+
}
|
|
4042
|
+
await new Promise((resolve13) => setTimeout(resolve13, delayMs));
|
|
4043
|
+
} catch (error) {
|
|
4044
|
+
if (error.message?.includes("does not exist")) {
|
|
4045
|
+
return { success: true, status: "DELETE_COMPLETE" };
|
|
4046
|
+
}
|
|
4047
|
+
throw error;
|
|
4012
4048
|
}
|
|
4013
|
-
}
|
|
4049
|
+
}
|
|
4050
|
+
return { success: false, status: "TIMEOUT", reason: "Timeout waiting for stack operation" };
|
|
4014
4051
|
}
|
|
4015
4052
|
}
|
|
4016
|
-
var
|
|
4053
|
+
var init_cloudformation = __esm(() => {
|
|
4017
4054
|
init_client();
|
|
4018
4055
|
});
|
|
4019
4056
|
|
|
@@ -4106,18 +4143,20 @@ class CloudFrontClient {
|
|
|
4106
4143
|
const distributions = [];
|
|
4107
4144
|
const distList = result.DistributionList || result;
|
|
4108
4145
|
const items = distList.Items;
|
|
4109
|
-
|
|
4110
|
-
if (
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4146
|
+
let summaries = [];
|
|
4147
|
+
if (Array.isArray(items)) {
|
|
4148
|
+
summaries = items;
|
|
4149
|
+
} else if (items?.DistributionSummary) {
|
|
4150
|
+
summaries = Array.isArray(items.DistributionSummary) ? items.DistributionSummary : [items.DistributionSummary];
|
|
4151
|
+
}
|
|
4152
|
+
distributions.push(...summaries.map((item) => ({
|
|
4153
|
+
Id: item.Id,
|
|
4154
|
+
ARN: item.ARN,
|
|
4155
|
+
Status: item.Status,
|
|
4156
|
+
DomainName: item.DomainName,
|
|
4157
|
+
Aliases: item.Aliases || undefined,
|
|
4158
|
+
Enabled: item.Enabled === "true" || item.Enabled === true
|
|
4159
|
+
})));
|
|
4121
4160
|
return distributions;
|
|
4122
4161
|
}
|
|
4123
4162
|
async getDistribution(distributionId) {
|
|
@@ -4445,6 +4484,70 @@ ${children}${indent}</${name}>
|
|
|
4445
4484
|
<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
|
|
4446
4485
|
${Object.entries(config6).filter(([k]) => !k.startsWith("@_")).map(([key, val]) => buildXmlElement(key, val, " ", "DistributionConfig")).join("")}</DistributionConfig>`;
|
|
4447
4486
|
}
|
|
4487
|
+
async ensureDynamicHttpMethods(distributionId) {
|
|
4488
|
+
const getResult = await this.client.request({
|
|
4489
|
+
service: "cloudfront",
|
|
4490
|
+
region: "us-east-1",
|
|
4491
|
+
method: "GET",
|
|
4492
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
4493
|
+
returnHeaders: true
|
|
4494
|
+
});
|
|
4495
|
+
const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
|
|
4496
|
+
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig || getResult.body;
|
|
4497
|
+
if (!currentConfig) {
|
|
4498
|
+
throw new Error("Failed to get current distribution config");
|
|
4499
|
+
}
|
|
4500
|
+
let changed = false;
|
|
4501
|
+
const dynamicMethods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"];
|
|
4502
|
+
const parseAllowedMethods = (allowedMethods) => {
|
|
4503
|
+
if (!allowedMethods)
|
|
4504
|
+
return [];
|
|
4505
|
+
const items = allowedMethods.Items;
|
|
4506
|
+
if (!items)
|
|
4507
|
+
return [];
|
|
4508
|
+
if (Array.isArray(items))
|
|
4509
|
+
return items.map(String);
|
|
4510
|
+
if (typeof items === "object" && items.Method) {
|
|
4511
|
+
const method = items.Method;
|
|
4512
|
+
return Array.isArray(method) ? method.map(String) : [String(method)];
|
|
4513
|
+
}
|
|
4514
|
+
if (items.Item) {
|
|
4515
|
+
return Array.isArray(items.Item) ? items.Item.map(String) : [String(items.Item)];
|
|
4516
|
+
}
|
|
4517
|
+
return [];
|
|
4518
|
+
};
|
|
4519
|
+
const defaultBehavior = currentConfig.DefaultCacheBehavior;
|
|
4520
|
+
if (defaultBehavior) {
|
|
4521
|
+
const allowed = parseAllowedMethods(defaultBehavior.AllowedMethods);
|
|
4522
|
+
if (!allowed.includes("POST")) {
|
|
4523
|
+
defaultBehavior.AllowedMethods = {
|
|
4524
|
+
Quantity: dynamicMethods.length,
|
|
4525
|
+
Items: { Method: dynamicMethods },
|
|
4526
|
+
CachedMethods: {
|
|
4527
|
+
Quantity: 2,
|
|
4528
|
+
Items: { Method: ["GET", "HEAD"] }
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4531
|
+
changed = true;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
if (!changed) {
|
|
4535
|
+
return false;
|
|
4536
|
+
}
|
|
4537
|
+
const configXml = this.buildDistributionConfigXml(currentConfig);
|
|
4538
|
+
await this.client.request({
|
|
4539
|
+
service: "cloudfront",
|
|
4540
|
+
region: "us-east-1",
|
|
4541
|
+
method: "PUT",
|
|
4542
|
+
path: `/2020-05-31/distribution/${distributionId}/config`,
|
|
4543
|
+
body: configXml,
|
|
4544
|
+
headers: {
|
|
4545
|
+
"Content-Type": "application/xml",
|
|
4546
|
+
"If-Match": etag
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
return true;
|
|
4550
|
+
}
|
|
4448
4551
|
async addAliases(distributionId, aliases, certificateArn) {
|
|
4449
4552
|
return this.updateDistribution({
|
|
4450
4553
|
distributionId,
|
|
@@ -6912,8 +7015,8 @@ class DnsProviderFactory {
|
|
|
6912
7015
|
const godaddyApiKey = process.env.GODADDY_API_KEY;
|
|
6913
7016
|
const godaddyApiSecret = process.env.GODADDY_API_SECRET;
|
|
6914
7017
|
if (godaddyApiKey && godaddyApiSecret) {
|
|
6915
|
-
const
|
|
6916
|
-
this.addGoDaddy(godaddyApiKey, godaddyApiSecret,
|
|
7018
|
+
const env2 = process.env.GODADDY_ENVIRONMENT;
|
|
7019
|
+
this.addGoDaddy(godaddyApiKey, godaddyApiSecret, env2);
|
|
6917
7020
|
}
|
|
6918
7021
|
const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
6919
7022
|
if (cloudflareApiToken) {
|
|
@@ -7419,12 +7522,22 @@ function generateExternalDnsStaticSiteTemplate(config6) {
|
|
|
7419
7522
|
defaultRootObject = "index.html",
|
|
7420
7523
|
errorDocument = "404.html",
|
|
7421
7524
|
passthroughUrls = false,
|
|
7422
|
-
singlePageApp = false
|
|
7525
|
+
singlePageApp = false,
|
|
7526
|
+
dynamicApp = false,
|
|
7527
|
+
computeOriginDomain,
|
|
7528
|
+
computeOriginPort = 3008,
|
|
7529
|
+
computeOriginId = "app-compute",
|
|
7530
|
+
retainOnStackDelete = false
|
|
7423
7531
|
} = config6;
|
|
7532
|
+
const retainPolicy = retainOnStackDelete ? { DeletionPolicy: "Retain", UpdateReplacePolicy: "Retain" } : {};
|
|
7533
|
+
const useComputeOrigin = dynamicApp && !!computeOriginDomain;
|
|
7534
|
+
const defaultAllowedMethods = useComputeOrigin ? ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"] : ["GET", "HEAD"];
|
|
7535
|
+
const defaultCachedMethods = useComputeOrigin ? ["GET", "HEAD"] : ["GET", "HEAD"];
|
|
7424
7536
|
const resources = {};
|
|
7425
7537
|
const outputs = {};
|
|
7426
7538
|
resources.S3Bucket = {
|
|
7427
7539
|
Type: "AWS::S3::Bucket",
|
|
7540
|
+
...retainPolicy,
|
|
7428
7541
|
Properties: {
|
|
7429
7542
|
BucketName: bucketName,
|
|
7430
7543
|
PublicAccessBlockConfiguration: {
|
|
@@ -7449,6 +7562,7 @@ function generateExternalDnsStaticSiteTemplate(config6) {
|
|
|
7449
7562
|
};
|
|
7450
7563
|
resources.CloudFrontOAC = {
|
|
7451
7564
|
Type: "AWS::CloudFront::OriginAccessControl",
|
|
7565
|
+
...retainPolicy,
|
|
7452
7566
|
Properties: {
|
|
7453
7567
|
OriginAccessControlConfig: {
|
|
7454
7568
|
Name: `OAC-${bucketName}`,
|
|
@@ -7459,6 +7573,34 @@ function generateExternalDnsStaticSiteTemplate(config6) {
|
|
|
7459
7573
|
}
|
|
7460
7574
|
}
|
|
7461
7575
|
};
|
|
7576
|
+
let installRootRedirectLogicalId;
|
|
7577
|
+
if (passthroughUrls && useComputeOrigin) {
|
|
7578
|
+
installRootRedirectLogicalId = "InstallRootRedirectFunction";
|
|
7579
|
+
resources[installRootRedirectLogicalId] = {
|
|
7580
|
+
Type: "AWS::CloudFront::Function",
|
|
7581
|
+
Properties: {
|
|
7582
|
+
Name: { "Fn::Sub": "${AWS::StackName}-install-root-redirect" },
|
|
7583
|
+
AutoPublish: true,
|
|
7584
|
+
FunctionConfig: {
|
|
7585
|
+
Comment: "Redirect curl pantry.dev | bash (GET /) to /install.sh on S3",
|
|
7586
|
+
Runtime: "cloudfront-js-2.0"
|
|
7587
|
+
},
|
|
7588
|
+
FunctionCode: `function handler(event) {
|
|
7589
|
+
var request = event.request;
|
|
7590
|
+
if (request.uri === '/' || request.uri === '') {
|
|
7591
|
+
return {
|
|
7592
|
+
statusCode: 302,
|
|
7593
|
+
statusDescription: 'Found',
|
|
7594
|
+
headers: {
|
|
7595
|
+
location: { value: 'https://' + request.headers.host.value + '/install.sh' }
|
|
7596
|
+
}
|
|
7597
|
+
};
|
|
7598
|
+
}
|
|
7599
|
+
return request;
|
|
7600
|
+
}`
|
|
7601
|
+
}
|
|
7602
|
+
};
|
|
7603
|
+
}
|
|
7462
7604
|
if (!passthroughUrls) {
|
|
7463
7605
|
resources.UrlRewriteFunction = {
|
|
7464
7606
|
Type: "AWS::CloudFront::Function",
|
|
@@ -7487,38 +7629,72 @@ function generateExternalDnsStaticSiteTemplate(config6) {
|
|
|
7487
7629
|
}
|
|
7488
7630
|
};
|
|
7489
7631
|
}
|
|
7632
|
+
const s3Origin = {
|
|
7633
|
+
Id: `S3-${bucketName}`,
|
|
7634
|
+
DomainName: { "Fn::GetAtt": ["S3Bucket", "RegionalDomainName"] },
|
|
7635
|
+
S3OriginConfig: {
|
|
7636
|
+
OriginAccessIdentity: ""
|
|
7637
|
+
},
|
|
7638
|
+
OriginAccessControlId: { "Fn::GetAtt": ["CloudFrontOAC", "Id"] }
|
|
7639
|
+
};
|
|
7640
|
+
const computeOrigin = useComputeOrigin ? {
|
|
7641
|
+
Id: computeOriginId,
|
|
7642
|
+
DomainName: computeOriginDomain,
|
|
7643
|
+
CustomOriginConfig: {
|
|
7644
|
+
HTTPPort: computeOriginPort,
|
|
7645
|
+
HTTPSPort: 443,
|
|
7646
|
+
OriginProtocolPolicy: "http-only",
|
|
7647
|
+
OriginSSLProtocols: ["TLSv1.2"]
|
|
7648
|
+
}
|
|
7649
|
+
} : null;
|
|
7650
|
+
const defaultTargetOriginId = useComputeOrigin ? computeOriginId : `S3-${bucketName}`;
|
|
7490
7651
|
const distributionConfig = {
|
|
7491
7652
|
Enabled: true,
|
|
7492
7653
|
DefaultRootObject: defaultRootObject,
|
|
7493
7654
|
HttpVersion: "http2and3",
|
|
7494
7655
|
IPV6Enabled: true,
|
|
7495
7656
|
PriceClass: "PriceClass_100",
|
|
7496
|
-
Origins: [
|
|
7497
|
-
{
|
|
7498
|
-
Id: `S3-${bucketName}`,
|
|
7499
|
-
DomainName: { "Fn::GetAtt": ["S3Bucket", "RegionalDomainName"] },
|
|
7500
|
-
S3OriginConfig: {
|
|
7501
|
-
OriginAccessIdentity: ""
|
|
7502
|
-
},
|
|
7503
|
-
OriginAccessControlId: { "Fn::GetAtt": ["CloudFrontOAC", "Id"] }
|
|
7504
|
-
}
|
|
7505
|
-
],
|
|
7657
|
+
Origins: computeOrigin ? [computeOrigin, s3Origin] : [s3Origin],
|
|
7506
7658
|
DefaultCacheBehavior: {
|
|
7507
|
-
TargetOriginId:
|
|
7659
|
+
TargetOriginId: defaultTargetOriginId,
|
|
7508
7660
|
ViewerProtocolPolicy: "redirect-to-https",
|
|
7509
|
-
AllowedMethods:
|
|
7510
|
-
CachedMethods:
|
|
7661
|
+
AllowedMethods: defaultAllowedMethods,
|
|
7662
|
+
CachedMethods: defaultCachedMethods,
|
|
7511
7663
|
Compress: true,
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7664
|
+
...useComputeOrigin ? {
|
|
7665
|
+
CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
|
|
7666
|
+
OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac",
|
|
7667
|
+
...passthroughUrls && installRootRedirectLogicalId ? {
|
|
7668
|
+
FunctionAssociations: [{
|
|
7516
7669
|
EventType: "viewer-request",
|
|
7517
|
-
FunctionARN: { "Fn::GetAtt": [
|
|
7518
|
-
}
|
|
7519
|
-
|
|
7670
|
+
FunctionARN: { "Fn::GetAtt": [installRootRedirectLogicalId, "FunctionARN"] }
|
|
7671
|
+
}]
|
|
7672
|
+
} : {}
|
|
7673
|
+
} : {
|
|
7674
|
+
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
|
|
7675
|
+
...!passthroughUrls && {
|
|
7676
|
+
FunctionAssociations: [
|
|
7677
|
+
{
|
|
7678
|
+
EventType: "viewer-request",
|
|
7679
|
+
FunctionARN: { "Fn::GetAtt": ["UrlRewriteFunction", "FunctionARN"] }
|
|
7680
|
+
}
|
|
7681
|
+
]
|
|
7682
|
+
}
|
|
7520
7683
|
}
|
|
7521
7684
|
},
|
|
7685
|
+
...useComputeOrigin && passthroughUrls ? {
|
|
7686
|
+
CacheBehaviors: [
|
|
7687
|
+
{
|
|
7688
|
+
PathPattern: "/install.sh",
|
|
7689
|
+
TargetOriginId: `S3-${bucketName}`,
|
|
7690
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
7691
|
+
AllowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
7692
|
+
CachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
7693
|
+
Compress: true,
|
|
7694
|
+
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
|
|
7695
|
+
}
|
|
7696
|
+
]
|
|
7697
|
+
} : {},
|
|
7522
7698
|
CustomErrorResponses: singlePageApp ? [
|
|
7523
7699
|
{
|
|
7524
7700
|
ErrorCode: 403,
|
|
@@ -7561,7 +7737,13 @@ function generateExternalDnsStaticSiteTemplate(config6) {
|
|
|
7561
7737
|
}
|
|
7562
7738
|
resources.CloudFrontDistribution = {
|
|
7563
7739
|
Type: "AWS::CloudFront::Distribution",
|
|
7564
|
-
|
|
7740
|
+
...retainPolicy,
|
|
7741
|
+
DependsOn: [
|
|
7742
|
+
"S3Bucket",
|
|
7743
|
+
"CloudFrontOAC",
|
|
7744
|
+
...passthroughUrls && useComputeOrigin && installRootRedirectLogicalId ? [installRootRedirectLogicalId] : [],
|
|
7745
|
+
...!passthroughUrls ? ["UrlRewriteFunction"] : []
|
|
7746
|
+
],
|
|
7565
7747
|
Properties: {
|
|
7566
7748
|
DistributionConfig: distributionConfig
|
|
7567
7749
|
}
|
|
@@ -7576,6 +7758,7 @@ function generateExternalDnsStaticSiteTemplate(config6) {
|
|
|
7576
7758
|
};
|
|
7577
7759
|
resources.S3BucketPolicy = {
|
|
7578
7760
|
Type: "AWS::S3::BucketPolicy",
|
|
7761
|
+
...retainPolicy,
|
|
7579
7762
|
DependsOn: ["S3Bucket", "CloudFrontDistribution"],
|
|
7580
7763
|
Properties: {
|
|
7581
7764
|
Bucket: { Ref: "S3Bucket" },
|
|
@@ -7810,7 +7993,11 @@ async function deployStaticSiteWithExternalDns(config6) {
|
|
|
7810
7993
|
defaultRootObject: config6.defaultRootObject,
|
|
7811
7994
|
errorDocument: config6.errorDocument,
|
|
7812
7995
|
passthroughUrls: config6.passthroughUrls,
|
|
7813
|
-
singlePageApp: config6.singlePageApp
|
|
7996
|
+
singlePageApp: config6.singlePageApp,
|
|
7997
|
+
dynamicApp: config6.dynamicApp,
|
|
7998
|
+
computeOriginDomain: config6.computeOriginDomain,
|
|
7999
|
+
computeOriginPort: config6.computeOriginPort,
|
|
8000
|
+
computeOriginId: config6.computeOriginId
|
|
7814
8001
|
});
|
|
7815
8002
|
const tags = Object.entries(config6.tags || {}).map(([Key, Value]) => ({ Key, Value }));
|
|
7816
8003
|
tags.push({ Key: "ManagedBy", Value: "ts-cloud" });
|
|
@@ -18819,6 +19006,13 @@ var RealtimePresets = {
|
|
|
18819
19006
|
}
|
|
18820
19007
|
}
|
|
18821
19008
|
};
|
|
19009
|
+
function resolveCloudProvider(config6) {
|
|
19010
|
+
if (config6.cloud?.provider)
|
|
19011
|
+
return config6.cloud.provider;
|
|
19012
|
+
if (config6.hetzner?.apiToken)
|
|
19013
|
+
return "hetzner";
|
|
19014
|
+
return "aws";
|
|
19015
|
+
}
|
|
18822
19016
|
|
|
18823
19017
|
class TemplateBuilder {
|
|
18824
19018
|
template;
|
|
@@ -18974,6 +19168,28 @@ function getTimestamp() {
|
|
|
18974
19168
|
function sanitizeName(name) {
|
|
18975
19169
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
18976
19170
|
}
|
|
19171
|
+
function resolveProjectStackName(config6, environment) {
|
|
19172
|
+
return config6.project.stackName ?? `${config6.project.slug}-${environment}`;
|
|
19173
|
+
}
|
|
19174
|
+
function resolveSiteStackName(config6, siteKey, site, environment) {
|
|
19175
|
+
return site.stackName ?? `${config6.project.slug}-${environment}-${siteKey}-site`;
|
|
19176
|
+
}
|
|
19177
|
+
function resolveSiteResourceName(config6, siteKey) {
|
|
19178
|
+
return `${config6.project.slug}-${siteKey}`;
|
|
19179
|
+
}
|
|
19180
|
+
function resolveSiteBucketName(slug, environment, siteKey, explicitBucket) {
|
|
19181
|
+
if (explicitBucket)
|
|
19182
|
+
return explicitBucket;
|
|
19183
|
+
if (siteKey === "main")
|
|
19184
|
+
return `${slug}-${environment}-site`;
|
|
19185
|
+
return `${slug}-${environment}-${siteKey}`;
|
|
19186
|
+
}
|
|
19187
|
+
function resolveStorageBucketName(slug, environment, bucketKey, explicitBucket) {
|
|
19188
|
+
return explicitBucket ?? `${slug}-${environment}-${bucketKey}`;
|
|
19189
|
+
}
|
|
19190
|
+
function resolveDeployBucketName(slug, environment) {
|
|
19191
|
+
return `${slug}-${environment}-deploy`;
|
|
19192
|
+
}
|
|
18977
19193
|
|
|
18978
19194
|
class DependencyGraph {
|
|
18979
19195
|
nodes = new Map;
|
|
@@ -19322,6 +19538,7 @@ class Storage {
|
|
|
19322
19538
|
static createBucket(options) {
|
|
19323
19539
|
const {
|
|
19324
19540
|
name,
|
|
19541
|
+
bucketName: explicitBucketName,
|
|
19325
19542
|
slug,
|
|
19326
19543
|
environment,
|
|
19327
19544
|
public: isPublic = false,
|
|
@@ -19332,7 +19549,7 @@ class Storage {
|
|
|
19332
19549
|
cors,
|
|
19333
19550
|
lifecycleRules
|
|
19334
19551
|
} = options;
|
|
19335
|
-
const resourceName = generateResourceName({
|
|
19552
|
+
const resourceName = explicitBucketName ?? generateResourceName({
|
|
19336
19553
|
slug,
|
|
19337
19554
|
environment,
|
|
19338
19555
|
resourceType: "s3",
|
|
@@ -23470,7 +23687,8 @@ echo "Server setup complete!"
|
|
|
23470
23687
|
runtime = "bun",
|
|
23471
23688
|
runtimeVersion = "latest",
|
|
23472
23689
|
systemPackages = [],
|
|
23473
|
-
database
|
|
23690
|
+
database,
|
|
23691
|
+
caddyfile
|
|
23474
23692
|
} = options;
|
|
23475
23693
|
const packages = new Set(systemPackages);
|
|
23476
23694
|
if (database === "sqlite")
|
|
@@ -23528,7 +23746,57 @@ ln -sf /root/.deno/bin/deno /usr/local/bin/deno
|
|
|
23528
23746
|
script += `
|
|
23529
23747
|
# Reserved root for site deploys (each site lands at /var/www/<site>/)
|
|
23530
23748
|
mkdir -p /var/www
|
|
23749
|
+
`;
|
|
23750
|
+
if (caddyfile) {
|
|
23751
|
+
const escaped = caddyfile.replace(/\$/g, "\\$");
|
|
23752
|
+
script += `
|
|
23753
|
+
# Caddy (reverse proxy + automatic TLS via Let's Encrypt)
|
|
23754
|
+
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
|
23755
|
+
curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=\${ARCH}" -o /usr/local/bin/caddy
|
|
23756
|
+
chmod +x /usr/local/bin/caddy
|
|
23757
|
+
|
|
23758
|
+
# Dedicated service account
|
|
23759
|
+
getent group caddy >/dev/null || groupadd --system caddy
|
|
23760
|
+
getent passwd caddy >/dev/null || useradd --system --gid caddy \\
|
|
23761
|
+
--create-home --home-dir /var/lib/caddy \\
|
|
23762
|
+
--shell /usr/sbin/nologin --comment "Caddy web server" caddy
|
|
23763
|
+
|
|
23764
|
+
mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
|
|
23765
|
+
chown -R caddy:caddy /var/lib/caddy /var/log/caddy
|
|
23766
|
+
|
|
23767
|
+
cat > /etc/systemd/system/caddy.service <<'CADDY_UNIT_EOF'
|
|
23768
|
+
[Unit]
|
|
23769
|
+
Description=Caddy
|
|
23770
|
+
Documentation=https://caddyserver.com/docs/
|
|
23771
|
+
After=network.target network-online.target
|
|
23772
|
+
Requires=network-online.target
|
|
23773
|
+
|
|
23774
|
+
[Service]
|
|
23775
|
+
Type=notify
|
|
23776
|
+
User=caddy
|
|
23777
|
+
Group=caddy
|
|
23778
|
+
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
|
|
23779
|
+
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
|
|
23780
|
+
TimeoutStopSec=5s
|
|
23781
|
+
LimitNOFILE=1048576
|
|
23782
|
+
PrivateTmp=true
|
|
23783
|
+
ProtectSystem=full
|
|
23784
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
23785
|
+
|
|
23786
|
+
[Install]
|
|
23787
|
+
WantedBy=multi-user.target
|
|
23788
|
+
CADDY_UNIT_EOF
|
|
23531
23789
|
|
|
23790
|
+
cat > /etc/caddy/Caddyfile <<'CADDY_CONFIG_EOF'
|
|
23791
|
+
${escaped}
|
|
23792
|
+
CADDY_CONFIG_EOF
|
|
23793
|
+
|
|
23794
|
+
systemctl daemon-reload
|
|
23795
|
+
systemctl enable caddy
|
|
23796
|
+
systemctl start caddy
|
|
23797
|
+
`;
|
|
23798
|
+
}
|
|
23799
|
+
script += `
|
|
23532
23800
|
echo "ts-cloud bootstrap complete — instance is ready for site deploys"
|
|
23533
23801
|
`;
|
|
23534
23802
|
return script;
|
|
@@ -29998,8 +30266,8 @@ class Messaging {
|
|
|
29998
30266
|
return topic;
|
|
29999
30267
|
}
|
|
30000
30268
|
static FilterPolicies = {
|
|
30001
|
-
eventType: (
|
|
30002
|
-
eventType:
|
|
30269
|
+
eventType: (types2) => ({
|
|
30270
|
+
eventType: types2
|
|
30003
30271
|
}),
|
|
30004
30272
|
status: (statuses) => ({
|
|
30005
30273
|
status: statuses
|
|
@@ -62098,6 +62366,125 @@ class InfrastructureGenerator {
|
|
|
62098
62366
|
const port = Number(configuredPort || 3008);
|
|
62099
62367
|
return Number.isFinite(port) && port > 0 ? port : 3008;
|
|
62100
62368
|
}
|
|
62369
|
+
defaultComputeCachePathPatterns() {
|
|
62370
|
+
return [
|
|
62371
|
+
"/api/*",
|
|
62372
|
+
"/auth/*",
|
|
62373
|
+
"/publisher/api/*",
|
|
62374
|
+
"/publisher/*",
|
|
62375
|
+
"/publish",
|
|
62376
|
+
"/publish/*",
|
|
62377
|
+
"/analytics/*",
|
|
62378
|
+
"/packages/*",
|
|
62379
|
+
"/npm/*",
|
|
62380
|
+
"/zig/*",
|
|
62381
|
+
"/php/*",
|
|
62382
|
+
"/commits/*",
|
|
62383
|
+
"/health",
|
|
62384
|
+
"/dashboard/*",
|
|
62385
|
+
"/search",
|
|
62386
|
+
"/login",
|
|
62387
|
+
"/logout",
|
|
62388
|
+
"/signup",
|
|
62389
|
+
"/account",
|
|
62390
|
+
"/account/*"
|
|
62391
|
+
];
|
|
62392
|
+
}
|
|
62393
|
+
shouldRouteStorageBucketToCompute(name, storageConfig) {
|
|
62394
|
+
return name === "public" || storageConfig.routeCompute === true;
|
|
62395
|
+
}
|
|
62396
|
+
resolveComputeCachePathPatterns(routes) {
|
|
62397
|
+
if (routes && routes.length > 0) {
|
|
62398
|
+
return routes;
|
|
62399
|
+
}
|
|
62400
|
+
return this.defaultComputeCachePathPatterns();
|
|
62401
|
+
}
|
|
62402
|
+
createComputeCacheBehavior(pathPattern, apiOriginId) {
|
|
62403
|
+
return {
|
|
62404
|
+
PathPattern: pathPattern,
|
|
62405
|
+
TargetOriginId: apiOriginId,
|
|
62406
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
62407
|
+
AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
|
|
62408
|
+
CachedMethods: ["GET", "HEAD"],
|
|
62409
|
+
Compress: true,
|
|
62410
|
+
CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
|
|
62411
|
+
OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac"
|
|
62412
|
+
};
|
|
62413
|
+
}
|
|
62414
|
+
appendComputeAppOrigin(origins, cacheBehaviors, extraDependsOn, slug, env, pathPatterns) {
|
|
62415
|
+
const appEipId = this.serverEipLogicalIds.get("app");
|
|
62416
|
+
const appInstanceId = this.serverEipLogicalIds.get("appInstance");
|
|
62417
|
+
if (!appEipId) {
|
|
62418
|
+
return;
|
|
62419
|
+
}
|
|
62420
|
+
const apiOriginId = `EC2-${slug}-${env}-api`;
|
|
62421
|
+
const serverRegion = this.mergedConfig.infrastructure?.servers?.app?.region || this.mergedConfig.project?.region || "us-east-1";
|
|
62422
|
+
const dnsSuffix = serverRegion === "us-east-1" ? ".compute-1.amazonaws.com" : `.${serverRegion}.compute.amazonaws.com`;
|
|
62423
|
+
const originDomainName = {
|
|
62424
|
+
"Fn::Join": ["", [
|
|
62425
|
+
"ec2-",
|
|
62426
|
+
{ "Fn::Join": ["-", { "Fn::Split": [".", { Ref: appEipId }] }] },
|
|
62427
|
+
dnsSuffix
|
|
62428
|
+
]]
|
|
62429
|
+
};
|
|
62430
|
+
const apiOriginPort = this.resolveApiOriginPort();
|
|
62431
|
+
origins.push({
|
|
62432
|
+
Id: apiOriginId,
|
|
62433
|
+
DomainName: originDomainName,
|
|
62434
|
+
CustomOriginConfig: {
|
|
62435
|
+
HTTPPort: apiOriginPort,
|
|
62436
|
+
HTTPSPort: 443,
|
|
62437
|
+
OriginProtocolPolicy: "http-only",
|
|
62438
|
+
OriginSSLProtocols: ["TLSv1.2"]
|
|
62439
|
+
}
|
|
62440
|
+
});
|
|
62441
|
+
extraDependsOn.push(appEipId);
|
|
62442
|
+
if (appInstanceId) {
|
|
62443
|
+
extraDependsOn.push(appInstanceId);
|
|
62444
|
+
}
|
|
62445
|
+
for (const pathPattern of pathPatterns) {
|
|
62446
|
+
cacheBehaviors.push(this.createComputeCacheBehavior(pathPattern, apiOriginId));
|
|
62447
|
+
}
|
|
62448
|
+
}
|
|
62449
|
+
buildCaddyfile(allSites) {
|
|
62450
|
+
const sitesWithDomain = allSites.filter(([, s]) => typeof s.domain === "string" && s.domain && typeof s.port === "number");
|
|
62451
|
+
if (sitesWithDomain.length === 0)
|
|
62452
|
+
return;
|
|
62453
|
+
const byDomain = new Map;
|
|
62454
|
+
for (const [, site] of sitesWithDomain) {
|
|
62455
|
+
const list = byDomain.get(site.domain) ?? [];
|
|
62456
|
+
list.push({ port: site.port, path: site.path });
|
|
62457
|
+
byDomain.set(site.domain, list);
|
|
62458
|
+
}
|
|
62459
|
+
const blocks = [];
|
|
62460
|
+
for (const [domain, sites] of byDomain) {
|
|
62461
|
+
const sorted = [...sites].sort((a, b) => {
|
|
62462
|
+
const aIsCatchAll = !a.path || a.path === "/";
|
|
62463
|
+
const bIsCatchAll = !b.path || b.path === "/";
|
|
62464
|
+
if (aIsCatchAll && !bIsCatchAll)
|
|
62465
|
+
return 1;
|
|
62466
|
+
if (!aIsCatchAll && bIsCatchAll)
|
|
62467
|
+
return -1;
|
|
62468
|
+
return (b.path?.length ?? 0) - (a.path?.length ?? 0);
|
|
62469
|
+
});
|
|
62470
|
+
const handles = sorted.map((s) => {
|
|
62471
|
+
const isCatchAll = !s.path || s.path === "/";
|
|
62472
|
+
const inner = `reverse_proxy localhost:${s.port}`;
|
|
62473
|
+
return isCatchAll ? ` handle {
|
|
62474
|
+
${inner}
|
|
62475
|
+
}` : ` handle ${s.path} {
|
|
62476
|
+
${inner}
|
|
62477
|
+
}`;
|
|
62478
|
+
});
|
|
62479
|
+
blocks.push(`${domain} {
|
|
62480
|
+
${handles.join(`
|
|
62481
|
+
`)}
|
|
62482
|
+
}`);
|
|
62483
|
+
}
|
|
62484
|
+
return blocks.join(`
|
|
62485
|
+
|
|
62486
|
+
`);
|
|
62487
|
+
}
|
|
62101
62488
|
normalizeMountPath(config6) {
|
|
62102
62489
|
const rawPath = config6?.mountPath || config6?.path;
|
|
62103
62490
|
if (!rawPath || rawPath === "/")
|
|
@@ -62785,11 +63172,13 @@ class InfrastructureGenerator {
|
|
|
62785
63172
|
if (!this.builder.hasResource("VPC")) {
|
|
62786
63173
|
this.generateNetworkInfrastructure(slug, env);
|
|
62787
63174
|
}
|
|
63175
|
+
const caddyfile = this.buildCaddyfile(allSites);
|
|
62788
63176
|
const userData = Compute.UserData.generateBunAppScript({
|
|
62789
63177
|
runtime: compute.runtime || "bun",
|
|
62790
63178
|
runtimeVersion: compute.runtimeVersion || "latest",
|
|
62791
63179
|
systemPackages: compute.systemPackages,
|
|
62792
|
-
database: dbEngine
|
|
63180
|
+
database: dbEngine,
|
|
63181
|
+
caddyfile
|
|
62793
63182
|
});
|
|
62794
63183
|
const sitePorts = allSites.map(([, s]) => s.port).filter((p) => typeof p === "number" && ![80, 443].includes(p));
|
|
62795
63184
|
const apiOriginPort = this.resolveApiOriginPort();
|
|
@@ -63003,11 +63392,12 @@ class InfrastructureGenerator {
|
|
|
63003
63392
|
})).filter((bucket) => bucket.name !== "public" && bucket.config.website && bucket.mountPath);
|
|
63004
63393
|
for (const [name, storageConfig] of Object.entries(this.mergedConfig.infrastructure.storage)) {
|
|
63005
63394
|
const serveViaCloudFront = !!(storageConfig.website && sharedOacLogicalId);
|
|
63395
|
+
const physicalBucketName = storageConfig.bucket ?? `${slug}-${env}-${name}`;
|
|
63006
63396
|
const { bucket, logicalId } = Storage.createBucket({
|
|
63007
63397
|
slug,
|
|
63008
63398
|
name,
|
|
63009
63399
|
environment: env,
|
|
63010
|
-
bucketName:
|
|
63400
|
+
bucketName: physicalBucketName,
|
|
63011
63401
|
versioning: storageConfig.versioning,
|
|
63012
63402
|
encryption: storageConfig.encryption,
|
|
63013
63403
|
public: serveViaCloudFront ? false : storageConfig.public
|
|
@@ -63090,45 +63480,8 @@ else if (!uri.includes('.')) { request.uri += '.html'; } return request; }`
|
|
|
63090
63480
|
}];
|
|
63091
63481
|
const cacheBehaviors = [];
|
|
63092
63482
|
const extraDependsOn = [];
|
|
63093
|
-
if (name
|
|
63094
|
-
|
|
63095
|
-
const appInstanceId = this.serverEipLogicalIds.get("appInstance");
|
|
63096
|
-
if (appEipId) {
|
|
63097
|
-
const apiOriginId = `EC2-${slug}-${env}-api`;
|
|
63098
|
-
const serverRegion = this.mergedConfig.infrastructure?.servers?.app?.region || this.mergedConfig.project?.region || "us-east-1";
|
|
63099
|
-
const dnsSuffix = serverRegion === "us-east-1" ? ".compute-1.amazonaws.com" : `.${serverRegion}.compute.amazonaws.com`;
|
|
63100
|
-
const originDomainName = {
|
|
63101
|
-
"Fn::Join": ["", [
|
|
63102
|
-
"ec2-",
|
|
63103
|
-
{ "Fn::Join": ["-", { "Fn::Split": [".", { Ref: appEipId }] }] },
|
|
63104
|
-
dnsSuffix
|
|
63105
|
-
]]
|
|
63106
|
-
};
|
|
63107
|
-
const apiOriginPort = this.resolveApiOriginPort();
|
|
63108
|
-
origins.push({
|
|
63109
|
-
Id: apiOriginId,
|
|
63110
|
-
DomainName: originDomainName,
|
|
63111
|
-
CustomOriginConfig: {
|
|
63112
|
-
HTTPPort: apiOriginPort,
|
|
63113
|
-
HTTPSPort: 443,
|
|
63114
|
-
OriginProtocolPolicy: "http-only",
|
|
63115
|
-
OriginSSLProtocols: ["TLSv1.2"]
|
|
63116
|
-
}
|
|
63117
|
-
});
|
|
63118
|
-
extraDependsOn.push(appEipId);
|
|
63119
|
-
if (appInstanceId)
|
|
63120
|
-
extraDependsOn.push(appInstanceId);
|
|
63121
|
-
cacheBehaviors.push({
|
|
63122
|
-
PathPattern: "/api/*",
|
|
63123
|
-
TargetOriginId: apiOriginId,
|
|
63124
|
-
ViewerProtocolPolicy: "redirect-to-https",
|
|
63125
|
-
AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
|
|
63126
|
-
CachedMethods: ["GET", "HEAD"],
|
|
63127
|
-
Compress: true,
|
|
63128
|
-
CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
|
|
63129
|
-
OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac"
|
|
63130
|
-
});
|
|
63131
|
-
}
|
|
63483
|
+
if (this.shouldRouteStorageBucketToCompute(name, storageConfig)) {
|
|
63484
|
+
this.appendComputeAppOrigin(origins, cacheBehaviors, extraDependsOn, slug, env, this.resolveComputeCachePathPatterns(storageConfig.computeRoutes));
|
|
63132
63485
|
for (const mountedBucket of pathMountedWebsiteBuckets) {
|
|
63133
63486
|
const mountedOriginId = `S3-${slug}-${env}-${mountedBucket.name}`;
|
|
63134
63487
|
const mountedFunctionLogicalId = `${slug}${env}${mountedBucket.name}PathMountRewrite`.replace(/[^a-zA-Z0-9]/g, "");
|
|
@@ -63233,7 +63586,7 @@ else if (!uri.includes('.')) { request.uri += '.html'; } return request; }`
|
|
|
63233
63586
|
}
|
|
63234
63587
|
}
|
|
63235
63588
|
});
|
|
63236
|
-
if (name
|
|
63589
|
+
if (this.shouldRouteStorageBucketToCompute(name, storageConfig)) {
|
|
63237
63590
|
for (const mountedBucket of pathMountedWebsiteBuckets) {
|
|
63238
63591
|
const mountedPolicyLogicalId = `${mountedBucket.logicalId}CloudFrontPolicy`;
|
|
63239
63592
|
this.builder.addResource(mountedPolicyLogicalId, {
|
|
@@ -63377,15 +63730,78 @@ else if (!uri.includes('.')) { request.uri += '.html'; } return request; }`
|
|
|
63377
63730
|
}
|
|
63378
63731
|
if (this.mergedConfig.infrastructure?.cdn) {
|
|
63379
63732
|
for (const [name, cdnConfig] of Object.entries(this.mergedConfig.infrastructure.cdn)) {
|
|
63380
|
-
|
|
63381
|
-
|
|
63382
|
-
|
|
63383
|
-
|
|
63384
|
-
|
|
63385
|
-
|
|
63733
|
+
if (!cdnConfig.origin) {
|
|
63734
|
+
continue;
|
|
63735
|
+
}
|
|
63736
|
+
const customDomain = typeof cdnConfig.customDomain === "string" ? cdnConfig.customDomain : cdnConfig.customDomain?.domain || cdnConfig.domain;
|
|
63737
|
+
const explicitCertificateArn = typeof cdnConfig.customDomain === "object" ? cdnConfig.customDomain?.certificateArn : cdnConfig.certificateArn;
|
|
63738
|
+
const resolvedCertArn = explicitCertificateArn || cfCertificateArn;
|
|
63739
|
+
const distLogicalId = `${slug}${env}${name}CDN`.replace(/[^a-zA-Z0-9]/g, "");
|
|
63740
|
+
const originId = `S3-${slug}-${env}-${name}-cdn`;
|
|
63741
|
+
const origins = [{
|
|
63742
|
+
Id: originId,
|
|
63743
|
+
DomainName: cdnConfig.origin,
|
|
63744
|
+
OriginPath: "",
|
|
63745
|
+
S3OriginConfig: {
|
|
63746
|
+
OriginAccessIdentity: ""
|
|
63386
63747
|
}
|
|
63387
|
-
}
|
|
63388
|
-
|
|
63748
|
+
}];
|
|
63749
|
+
const cacheBehaviors = [];
|
|
63750
|
+
const extraDependsOn = [];
|
|
63751
|
+
if (cdnConfig.routeCompute) {
|
|
63752
|
+
this.appendComputeAppOrigin(origins, cacheBehaviors, extraDependsOn, slug, env, this.resolveComputeCachePathPatterns(cdnConfig.computeRoutes));
|
|
63753
|
+
}
|
|
63754
|
+
const viewerCertificate = resolvedCertArn && customDomain ? {
|
|
63755
|
+
AcmCertificateArn: cfCertificateLogicalId && !explicitCertificateArn ? { Ref: cfCertificateLogicalId } : resolvedCertArn,
|
|
63756
|
+
SslSupportMethod: "sni-only",
|
|
63757
|
+
MinimumProtocolVersion: "TLSv1.2_2021"
|
|
63758
|
+
} : { CloudFrontDefaultCertificate: true };
|
|
63759
|
+
const distributionDependsOn = [...extraDependsOn];
|
|
63760
|
+
if (cfCertificateLogicalId && !explicitCertificateArn && customDomain) {
|
|
63761
|
+
distributionDependsOn.push(cfCertificateLogicalId);
|
|
63762
|
+
}
|
|
63763
|
+
const distribution = {
|
|
63764
|
+
Type: "AWS::CloudFront::Distribution",
|
|
63765
|
+
...distributionDependsOn.length > 0 ? { DependsOn: distributionDependsOn } : {},
|
|
63766
|
+
Properties: {
|
|
63767
|
+
DistributionConfig: {
|
|
63768
|
+
Enabled: true,
|
|
63769
|
+
Comment: `${slug} ${env} ${name} CDN`,
|
|
63770
|
+
DefaultRootObject: "index.html",
|
|
63771
|
+
Origins: origins,
|
|
63772
|
+
DefaultCacheBehavior: {
|
|
63773
|
+
TargetOriginId: originId,
|
|
63774
|
+
ViewerProtocolPolicy: "redirect-to-https",
|
|
63775
|
+
AllowedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
63776
|
+
CachedMethods: ["GET", "HEAD", "OPTIONS"],
|
|
63777
|
+
Compress: cdnConfig.compress !== false,
|
|
63778
|
+
CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
|
|
63779
|
+
},
|
|
63780
|
+
...cacheBehaviors.length > 0 ? { CacheBehaviors: cacheBehaviors } : {},
|
|
63781
|
+
...customDomain && resolvedCertArn ? { Aliases: [customDomain] } : {},
|
|
63782
|
+
ViewerCertificate: viewerCertificate,
|
|
63783
|
+
PriceClass: "PriceClass_100",
|
|
63784
|
+
HttpVersion: cdnConfig.http3 ? "http2and3" : "http2"
|
|
63785
|
+
}
|
|
63786
|
+
}
|
|
63787
|
+
};
|
|
63788
|
+
this.builder.addResource(distLogicalId, distribution);
|
|
63789
|
+
if (hostedZoneId && customDomain && resolvedCertArn) {
|
|
63790
|
+
const safeName = customDomain.replace(/\./g, "").replace(/[^a-zA-Z0-9]/g, "");
|
|
63791
|
+
this.builder.addResource(`${safeName}CdnARecord`, {
|
|
63792
|
+
Type: "AWS::Route53::RecordSet",
|
|
63793
|
+
DependsOn: [distLogicalId, ...cfCertificateLogicalId && !explicitCertificateArn ? [cfCertificateLogicalId] : []],
|
|
63794
|
+
Properties: {
|
|
63795
|
+
HostedZoneId: hostedZoneId,
|
|
63796
|
+
Name: customDomain,
|
|
63797
|
+
Type: "A",
|
|
63798
|
+
AliasTarget: {
|
|
63799
|
+
HostedZoneId: "Z2FDTNDATAQYW2",
|
|
63800
|
+
DNSName: { "Fn::GetAtt": [distLogicalId, "DomainName"] }
|
|
63801
|
+
}
|
|
63802
|
+
}
|
|
63803
|
+
});
|
|
63804
|
+
}
|
|
63389
63805
|
}
|
|
63390
63806
|
}
|
|
63391
63807
|
if (this.mergedConfig.infrastructure?.queues) {
|
|
@@ -65146,6 +65562,84 @@ function validateResourceLimits(template) {
|
|
|
65146
65562
|
}
|
|
65147
65563
|
// src/aws/index.ts
|
|
65148
65564
|
init_client();
|
|
65565
|
+
|
|
65566
|
+
// src/object-storage/index.ts
|
|
65567
|
+
init_s3();
|
|
65568
|
+
var DEFAULT_REGION = {
|
|
65569
|
+
aws: "us-east-1",
|
|
65570
|
+
backblaze: "us-west-004",
|
|
65571
|
+
hetzner: "fsn1"
|
|
65572
|
+
};
|
|
65573
|
+
function providerEndpoint(provider, region) {
|
|
65574
|
+
switch (provider) {
|
|
65575
|
+
case "backblaze":
|
|
65576
|
+
return `s3.${region}.backblazeb2.com`;
|
|
65577
|
+
case "hetzner":
|
|
65578
|
+
return `${region}.your-objectstorage.com`;
|
|
65579
|
+
case "aws":
|
|
65580
|
+
default:
|
|
65581
|
+
return;
|
|
65582
|
+
}
|
|
65583
|
+
}
|
|
65584
|
+
function env(...names) {
|
|
65585
|
+
for (const name of names) {
|
|
65586
|
+
const value = process.env[name];
|
|
65587
|
+
if (value)
|
|
65588
|
+
return value;
|
|
65589
|
+
}
|
|
65590
|
+
return;
|
|
65591
|
+
}
|
|
65592
|
+
function resolveCredentials3(provider, explicit) {
|
|
65593
|
+
if (explicit?.accessKeyId && explicit.secretAccessKey)
|
|
65594
|
+
return explicit;
|
|
65595
|
+
let accessKeyId;
|
|
65596
|
+
let secretAccessKey;
|
|
65597
|
+
if (provider === "backblaze") {
|
|
65598
|
+
accessKeyId = env("B2_APPLICATION_KEY_ID", "B2_KEY_ID", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID");
|
|
65599
|
+
secretAccessKey = env("B2_APPLICATION_KEY", "B2_SECRET_KEY", "S3_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY");
|
|
65600
|
+
} else if (provider === "hetzner") {
|
|
65601
|
+
accessKeyId = env("HETZNER_S3_ACCESS_KEY", "HETZNER_ACCESS_KEY", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID");
|
|
65602
|
+
secretAccessKey = env("HETZNER_S3_SECRET_KEY", "HETZNER_SECRET_KEY", "S3_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY");
|
|
65603
|
+
} else {
|
|
65604
|
+
accessKeyId = env("S3_ACCESS_KEY_ID");
|
|
65605
|
+
secretAccessKey = env("S3_SECRET_ACCESS_KEY");
|
|
65606
|
+
}
|
|
65607
|
+
if (accessKeyId && secretAccessKey) {
|
|
65608
|
+
return { accessKeyId, secretAccessKey, sessionToken: env("AWS_SESSION_TOKEN") };
|
|
65609
|
+
}
|
|
65610
|
+
return;
|
|
65611
|
+
}
|
|
65612
|
+
function resolveObjectStorage(config6 = {}) {
|
|
65613
|
+
const provider = config6.provider || env("OBJECT_STORAGE_PROVIDER", "STORAGE_PROVIDER") || "aws";
|
|
65614
|
+
const region = config6.region || (provider === "backblaze" ? env("B2_REGION") : undefined) || (provider === "hetzner" ? env("HETZNER_S3_REGION", "HETZNER_REGION") : undefined) || env("S3_REGION", "AWS_REGION", "AWS_DEFAULT_REGION") || DEFAULT_REGION[provider];
|
|
65615
|
+
const endpoint = config6.endpoint || env("S3_ENDPOINT") || providerEndpoint(provider, region);
|
|
65616
|
+
const forcePathStyle = config6.forcePathStyle ?? env("S3_FORCE_PATH_STYLE") === "true";
|
|
65617
|
+
const credentials = resolveCredentials3(provider, config6.credentials);
|
|
65618
|
+
const publicBaseUrl = (bucket) => {
|
|
65619
|
+
const base = endpoint || `s3.${region}.amazonaws.com`;
|
|
65620
|
+
return forcePathStyle ? `https://${base}/${bucket}` : `https://${bucket}.${base}`;
|
|
65621
|
+
};
|
|
65622
|
+
return {
|
|
65623
|
+
provider,
|
|
65624
|
+
region,
|
|
65625
|
+
endpoint,
|
|
65626
|
+
forcePathStyle,
|
|
65627
|
+
credentials,
|
|
65628
|
+
profile: config6.profile,
|
|
65629
|
+
publicBaseUrl
|
|
65630
|
+
};
|
|
65631
|
+
}
|
|
65632
|
+
function createObjectStorageClient(config6 = {}) {
|
|
65633
|
+
const resolved = resolveObjectStorage(config6);
|
|
65634
|
+
const options = {
|
|
65635
|
+
endpoint: resolved.endpoint,
|
|
65636
|
+
forcePathStyle: resolved.forcePathStyle,
|
|
65637
|
+
credentials: resolved.credentials
|
|
65638
|
+
};
|
|
65639
|
+
return new S3Client2(resolved.region, resolved.profile, options);
|
|
65640
|
+
}
|
|
65641
|
+
|
|
65642
|
+
// src/aws/index.ts
|
|
65149
65643
|
init_cloudformation();
|
|
65150
65644
|
|
|
65151
65645
|
// src/aws/cost-explorer.ts
|
|
@@ -68299,9 +68793,9 @@ init_client();
|
|
|
68299
68793
|
class SESClient {
|
|
68300
68794
|
client;
|
|
68301
68795
|
region;
|
|
68302
|
-
constructor(region = "us-east-1") {
|
|
68796
|
+
constructor(region = "us-east-1", credentials) {
|
|
68303
68797
|
this.region = region;
|
|
68304
|
-
this.client = new AWSClient;
|
|
68798
|
+
this.client = new AWSClient(credentials);
|
|
68305
68799
|
}
|
|
68306
68800
|
async createEmailIdentity(params) {
|
|
68307
68801
|
const result = await this.client.request({
|
|
@@ -80512,10 +81006,922 @@ function resolveDnsProvider(input) {
|
|
|
80512
81006
|
function fail(start, message) {
|
|
80513
81007
|
return { success: false, message, durationMs: Date.now() - start };
|
|
80514
81008
|
}
|
|
81009
|
+
// src/drivers/aws/driver.ts
|
|
81010
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
81011
|
+
init_cloudformation();
|
|
81012
|
+
init_s3();
|
|
81013
|
+
class AwsDriver {
|
|
81014
|
+
name = "aws";
|
|
81015
|
+
usesCloudFormation = true;
|
|
81016
|
+
region;
|
|
81017
|
+
constructor(options = {}) {
|
|
81018
|
+
this.region = options.region || "us-east-1";
|
|
81019
|
+
}
|
|
81020
|
+
resolveRegion(config6) {
|
|
81021
|
+
return config6.project.region || this.region;
|
|
81022
|
+
}
|
|
81023
|
+
async getComputeOutputs(options) {
|
|
81024
|
+
const region = this.resolveRegion(options.config);
|
|
81025
|
+
const stackName = resolveProjectStackName(options.config, options.environment);
|
|
81026
|
+
const cfn = new CloudFormationClient(region);
|
|
81027
|
+
const outputs = await cfn.getStackOutputs(stackName);
|
|
81028
|
+
return {
|
|
81029
|
+
deployBucketName: outputs.deployBucketName,
|
|
81030
|
+
appInstanceId: outputs.appInstanceId,
|
|
81031
|
+
appPublicIp: outputs.appPublicIp,
|
|
81032
|
+
sshUser: "ec2-user"
|
|
81033
|
+
};
|
|
81034
|
+
}
|
|
81035
|
+
async uploadRelease(options) {
|
|
81036
|
+
const region = this.resolveRegion(options.config);
|
|
81037
|
+
const outputs = await this.getComputeOutputs({
|
|
81038
|
+
config: options.config,
|
|
81039
|
+
environment: options.environment
|
|
81040
|
+
});
|
|
81041
|
+
const bucket = outputs.deployBucketName;
|
|
81042
|
+
if (!bucket) {
|
|
81043
|
+
throw new Error("No deployBucketName in stack outputs. Re-deploy infrastructure to add the staging bucket.");
|
|
81044
|
+
}
|
|
81045
|
+
const s32 = new S3Client2(region);
|
|
81046
|
+
await s32.putObject({
|
|
81047
|
+
bucket,
|
|
81048
|
+
key: options.remoteKey,
|
|
81049
|
+
body: readFileSync9(options.localPath),
|
|
81050
|
+
contentType: "application/gzip"
|
|
81051
|
+
});
|
|
81052
|
+
return { artifactRef: `s3://${bucket}/${options.remoteKey}` };
|
|
81053
|
+
}
|
|
81054
|
+
async findComputeTargets(options) {
|
|
81055
|
+
const region = this.region;
|
|
81056
|
+
const ec22 = new EC2Client(region);
|
|
81057
|
+
const filters = [
|
|
81058
|
+
{ Name: "tag:Project", Values: [options.slug] },
|
|
81059
|
+
{ Name: "tag:Environment", Values: [options.environment] },
|
|
81060
|
+
{ Name: "tag:Role", Values: [options.role || "app"] },
|
|
81061
|
+
{ Name: "instance-state-name", Values: ["running", "pending"] }
|
|
81062
|
+
];
|
|
81063
|
+
const result = await ec22.describeInstances({ Filters: filters });
|
|
81064
|
+
const targets = [];
|
|
81065
|
+
for (const reservation of result.Reservations || []) {
|
|
81066
|
+
for (const instance of reservation.Instances || []) {
|
|
81067
|
+
if (!instance.InstanceId)
|
|
81068
|
+
continue;
|
|
81069
|
+
const nameTag = instance.Tags?.find((tag) => tag.Key === "Name")?.Value;
|
|
81070
|
+
targets.push({
|
|
81071
|
+
id: instance.InstanceId,
|
|
81072
|
+
name: nameTag,
|
|
81073
|
+
publicIp: instance.PublicIpAddress,
|
|
81074
|
+
privateIp: instance.PrivateIpAddress,
|
|
81075
|
+
status: instance.State?.Name
|
|
81076
|
+
});
|
|
81077
|
+
}
|
|
81078
|
+
}
|
|
81079
|
+
return targets;
|
|
81080
|
+
}
|
|
81081
|
+
async runRemoteDeploy(options) {
|
|
81082
|
+
const region = this.region;
|
|
81083
|
+
const ssm2 = new SSMClient(region);
|
|
81084
|
+
if (options.targets.length > 0) {
|
|
81085
|
+
const sendResult = await ssm2.sendCommand({
|
|
81086
|
+
InstanceIds: options.targets.map((target) => target.id),
|
|
81087
|
+
DocumentName: "AWS-RunShellScript",
|
|
81088
|
+
Parameters: { commands: options.commands },
|
|
81089
|
+
TimeoutSeconds: options.timeoutSeconds || 600,
|
|
81090
|
+
Comment: options.comment
|
|
81091
|
+
});
|
|
81092
|
+
if (!sendResult.CommandId) {
|
|
81093
|
+
return { success: false, instanceCount: 0, perInstance: [], error: "Failed to send SSM command" };
|
|
81094
|
+
}
|
|
81095
|
+
return this.pollSsmCommand(ssm2, sendResult.CommandId, options.targets.length);
|
|
81096
|
+
}
|
|
81097
|
+
if (!options.tags || Object.keys(options.tags).length === 0) {
|
|
81098
|
+
return { success: false, instanceCount: 0, perInstance: [], error: "No targets or tags provided for AWS deploy" };
|
|
81099
|
+
}
|
|
81100
|
+
const result = await ssm2.sendCommandByTags({
|
|
81101
|
+
tags: options.tags,
|
|
81102
|
+
commands: options.commands,
|
|
81103
|
+
timeoutSeconds: options.timeoutSeconds || 600,
|
|
81104
|
+
comment: options.comment
|
|
81105
|
+
});
|
|
81106
|
+
return {
|
|
81107
|
+
success: result.success,
|
|
81108
|
+
instanceCount: result.instanceCount,
|
|
81109
|
+
perInstance: result.perInstance.map((item) => ({
|
|
81110
|
+
instanceId: item.instanceId,
|
|
81111
|
+
status: item.status,
|
|
81112
|
+
output: item.output,
|
|
81113
|
+
error: item.error
|
|
81114
|
+
})),
|
|
81115
|
+
error: result.error
|
|
81116
|
+
};
|
|
81117
|
+
}
|
|
81118
|
+
async pollSsmCommand(ssm2, commandId, expectedCount) {
|
|
81119
|
+
const pollInterval = 3000;
|
|
81120
|
+
const maxWait = 600000;
|
|
81121
|
+
const startTime = Date.now();
|
|
81122
|
+
const terminalStatuses = new Set(["Success", "Failed", "Cancelled", "TimedOut"]);
|
|
81123
|
+
let lastInvocations = [];
|
|
81124
|
+
while (Date.now() - startTime < maxWait) {
|
|
81125
|
+
await new Promise((resolve13) => setTimeout(resolve13, pollInterval));
|
|
81126
|
+
try {
|
|
81127
|
+
const invocations = await ssm2.listCommandInvocations({ CommandId: commandId, Details: true });
|
|
81128
|
+
lastInvocations = invocations;
|
|
81129
|
+
if (lastInvocations.length >= expectedCount && lastInvocations.every((i) => terminalStatuses.has(i.Status || ""))) {
|
|
81130
|
+
break;
|
|
81131
|
+
}
|
|
81132
|
+
} catch {}
|
|
81133
|
+
}
|
|
81134
|
+
const perInstance = lastInvocations.map((item) => ({
|
|
81135
|
+
instanceId: item.InstanceId,
|
|
81136
|
+
status: item.Status || "Unknown",
|
|
81137
|
+
output: item.StandardOutputContent,
|
|
81138
|
+
error: item.StandardErrorContent
|
|
81139
|
+
}));
|
|
81140
|
+
const success = perInstance.length > 0 && perInstance.every((item) => item.status === "Success");
|
|
81141
|
+
return {
|
|
81142
|
+
success,
|
|
81143
|
+
instanceCount: perInstance.length,
|
|
81144
|
+
perInstance,
|
|
81145
|
+
error: success ? undefined : "One or more SSM command invocations failed"
|
|
81146
|
+
};
|
|
81147
|
+
}
|
|
81148
|
+
}
|
|
81149
|
+
|
|
81150
|
+
// src/drivers/hetzner/driver.ts
|
|
81151
|
+
import { homedir as homedir7 } from "node:os";
|
|
81152
|
+
import { join as join13 } from "node:path";
|
|
81153
|
+
import { execSync } from "node:child_process";
|
|
81154
|
+
|
|
81155
|
+
// src/drivers/shared/caddyfile.ts
|
|
81156
|
+
function buildCaddyfile(sites) {
|
|
81157
|
+
const allSites = Object.entries(sites);
|
|
81158
|
+
const sitesWithDomain = allSites.filter(([, s]) => typeof s.domain === "string" && s.domain && typeof s.port === "number");
|
|
81159
|
+
if (sitesWithDomain.length === 0)
|
|
81160
|
+
return;
|
|
81161
|
+
const byDomain = new Map;
|
|
81162
|
+
for (const [, site] of sitesWithDomain) {
|
|
81163
|
+
const list = byDomain.get(site.domain) ?? [];
|
|
81164
|
+
list.push({ port: site.port, path: site.path });
|
|
81165
|
+
byDomain.set(site.domain, list);
|
|
81166
|
+
}
|
|
81167
|
+
const blocks = [];
|
|
81168
|
+
for (const [domain, domainSites] of byDomain) {
|
|
81169
|
+
const sorted = [...domainSites].sort((a, b) => {
|
|
81170
|
+
const aIsCatchAll = !a.path || a.path === "/";
|
|
81171
|
+
const bIsCatchAll = !b.path || b.path === "/";
|
|
81172
|
+
if (aIsCatchAll && !bIsCatchAll)
|
|
81173
|
+
return 1;
|
|
81174
|
+
if (!aIsCatchAll && bIsCatchAll)
|
|
81175
|
+
return -1;
|
|
81176
|
+
return (b.path?.length ?? 0) - (a.path?.length ?? 0);
|
|
81177
|
+
});
|
|
81178
|
+
const handles = sorted.map((s) => {
|
|
81179
|
+
const isCatchAll = !s.path || s.path === "/";
|
|
81180
|
+
const inner = `reverse_proxy localhost:${s.port}`;
|
|
81181
|
+
return isCatchAll ? ` handle {
|
|
81182
|
+
${inner}
|
|
81183
|
+
}` : ` handle ${s.path} {
|
|
81184
|
+
${inner}
|
|
81185
|
+
}`;
|
|
81186
|
+
});
|
|
81187
|
+
blocks.push(`${domain} {
|
|
81188
|
+
${handles.join(`
|
|
81189
|
+
`)}
|
|
81190
|
+
}`);
|
|
81191
|
+
}
|
|
81192
|
+
return blocks.join(`
|
|
81193
|
+
|
|
81194
|
+
`);
|
|
81195
|
+
}
|
|
81196
|
+
|
|
81197
|
+
// src/drivers/hetzner/client.ts
|
|
81198
|
+
var DEFAULT_API_URL = "https://api.hetzner.cloud/v1";
|
|
81199
|
+
|
|
81200
|
+
class HetznerClient {
|
|
81201
|
+
name = "hetzner";
|
|
81202
|
+
apiToken;
|
|
81203
|
+
baseUrl;
|
|
81204
|
+
fetchImpl;
|
|
81205
|
+
constructor(options) {
|
|
81206
|
+
this.apiToken = options.apiToken;
|
|
81207
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_API_URL;
|
|
81208
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
81209
|
+
}
|
|
81210
|
+
async request(method, path, body) {
|
|
81211
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
81212
|
+
method,
|
|
81213
|
+
headers: {
|
|
81214
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
81215
|
+
"Content-Type": "application/json"
|
|
81216
|
+
},
|
|
81217
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
81218
|
+
});
|
|
81219
|
+
const text = await response.text();
|
|
81220
|
+
const data = text ? JSON.parse(text) : {};
|
|
81221
|
+
if (!response.ok) {
|
|
81222
|
+
const message = data.error?.message || response.statusText || "Hetzner API error";
|
|
81223
|
+
throw new Error(`Hetzner API ${method} ${path}: ${message}`);
|
|
81224
|
+
}
|
|
81225
|
+
return data;
|
|
81226
|
+
}
|
|
81227
|
+
async listServers() {
|
|
81228
|
+
const data = await this.request("GET", "/servers");
|
|
81229
|
+
return data.servers;
|
|
81230
|
+
}
|
|
81231
|
+
async getServer(id) {
|
|
81232
|
+
const data = await this.request("GET", `/servers/${id}`);
|
|
81233
|
+
return data.server;
|
|
81234
|
+
}
|
|
81235
|
+
async createServer(options) {
|
|
81236
|
+
const data = await this.request("POST", "/servers", {
|
|
81237
|
+
name: options.name,
|
|
81238
|
+
server_type: options.serverType,
|
|
81239
|
+
image: options.image,
|
|
81240
|
+
location: options.location,
|
|
81241
|
+
datacenter: options.datacenter,
|
|
81242
|
+
ssh_keys: options.sshKeys,
|
|
81243
|
+
user_data: options.userData,
|
|
81244
|
+
labels: options.labels,
|
|
81245
|
+
firewalls: options.firewalls,
|
|
81246
|
+
start_after_create: true
|
|
81247
|
+
});
|
|
81248
|
+
return { server: data.server, action: data.action };
|
|
81249
|
+
}
|
|
81250
|
+
async deleteServer(id) {
|
|
81251
|
+
const data = await this.request("DELETE", `/servers/${id}`);
|
|
81252
|
+
return data.action;
|
|
81253
|
+
}
|
|
81254
|
+
async listFirewalls() {
|
|
81255
|
+
const data = await this.request("GET", "/firewalls");
|
|
81256
|
+
return data.firewalls;
|
|
81257
|
+
}
|
|
81258
|
+
async createFirewall(options) {
|
|
81259
|
+
const data = await this.request("POST", "/firewalls", {
|
|
81260
|
+
name: options.name,
|
|
81261
|
+
rules: options.rules,
|
|
81262
|
+
labels: options.labels,
|
|
81263
|
+
apply_to: options.applyTo
|
|
81264
|
+
});
|
|
81265
|
+
return { firewall: data.firewall, actions: data.actions };
|
|
81266
|
+
}
|
|
81267
|
+
async applyFirewallToResources(firewallId, applyTo) {
|
|
81268
|
+
const data = await this.request("POST", `/firewalls/${firewallId}/actions/apply_to_resources`, {
|
|
81269
|
+
apply_to: applyTo
|
|
81270
|
+
});
|
|
81271
|
+
return data.actions;
|
|
81272
|
+
}
|
|
81273
|
+
async listSshKeys() {
|
|
81274
|
+
const data = await this.request("GET", "/ssh_keys");
|
|
81275
|
+
return data.ssh_keys;
|
|
81276
|
+
}
|
|
81277
|
+
async waitForAction(actionId, options) {
|
|
81278
|
+
const pollInterval = options?.pollIntervalMs ?? 2000;
|
|
81279
|
+
const maxWait = options?.maxWaitMs ?? 300000;
|
|
81280
|
+
const start = Date.now();
|
|
81281
|
+
while (Date.now() - start < maxWait) {
|
|
81282
|
+
const data = await this.request("GET", `/actions/${actionId}`);
|
|
81283
|
+
if (data.action.status === "success")
|
|
81284
|
+
return data.action;
|
|
81285
|
+
if (data.action.status === "error") {
|
|
81286
|
+
throw new Error(data.action.error?.message || "Hetzner action failed");
|
|
81287
|
+
}
|
|
81288
|
+
await new Promise((resolve13) => setTimeout(resolve13, pollInterval));
|
|
81289
|
+
}
|
|
81290
|
+
throw new Error(`Timed out waiting for Hetzner action ${actionId}`);
|
|
81291
|
+
}
|
|
81292
|
+
async waitForServerRunning(serverId, options) {
|
|
81293
|
+
const pollInterval = options?.pollIntervalMs ?? 3000;
|
|
81294
|
+
const maxWait = options?.maxWaitMs ?? 600000;
|
|
81295
|
+
const start = Date.now();
|
|
81296
|
+
while (Date.now() - start < maxWait) {
|
|
81297
|
+
const server = await this.getServer(serverId);
|
|
81298
|
+
if (server.status === "running")
|
|
81299
|
+
return server;
|
|
81300
|
+
await new Promise((resolve13) => setTimeout(resolve13, pollInterval));
|
|
81301
|
+
}
|
|
81302
|
+
throw new Error(`Timed out waiting for server ${serverId} to reach running state`);
|
|
81303
|
+
}
|
|
81304
|
+
}
|
|
81305
|
+
function resolveHetznerApiToken(configToken) {
|
|
81306
|
+
const token = configToken || process.env.HCLOUD_TOKEN || process.env.HETZNER_API_TOKEN;
|
|
81307
|
+
if (!token) {
|
|
81308
|
+
throw new Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");
|
|
81309
|
+
}
|
|
81310
|
+
return token;
|
|
81311
|
+
}
|
|
81312
|
+
|
|
81313
|
+
// src/drivers/hetzner/cloud-init.ts
|
|
81314
|
+
function generateUbuntuAppCloudInit(options = {}) {
|
|
81315
|
+
const {
|
|
81316
|
+
runtime = "bun",
|
|
81317
|
+
runtimeVersion = "latest",
|
|
81318
|
+
systemPackages = [],
|
|
81319
|
+
database,
|
|
81320
|
+
caddyfile
|
|
81321
|
+
} = options;
|
|
81322
|
+
const packages = new Set(systemPackages);
|
|
81323
|
+
if (database === "sqlite")
|
|
81324
|
+
packages.add("sqlite3");
|
|
81325
|
+
else if (database === "mysql")
|
|
81326
|
+
packages.add("mysql-client");
|
|
81327
|
+
else if (database === "postgres")
|
|
81328
|
+
packages.add("postgresql-client");
|
|
81329
|
+
let script = `#!/bin/bash
|
|
81330
|
+
set -euo pipefail
|
|
81331
|
+
|
|
81332
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
81333
|
+
apt-get update -y
|
|
81334
|
+
apt-get upgrade -y
|
|
81335
|
+
apt-get install -y curl tar gzip unzip git ca-certificates
|
|
81336
|
+
`;
|
|
81337
|
+
if (packages.size > 0) {
|
|
81338
|
+
script += `
|
|
81339
|
+
apt-get install -y ${[...packages].join(" ")}
|
|
81340
|
+
`;
|
|
81341
|
+
}
|
|
81342
|
+
if (runtime === "bun") {
|
|
81343
|
+
script += `
|
|
81344
|
+
export BUN_INSTALL="/root/.bun"
|
|
81345
|
+
curl -fsSL https://bun.sh/install | bash${runtimeVersion === "latest" ? "" : ` -s "bun-v${runtimeVersion}"`}
|
|
81346
|
+
ln -sf /root/.bun/bin/bun /usr/local/bin/bun
|
|
81347
|
+
echo 'export BUN_INSTALL="/root/.bun"' > /etc/profile.d/bun.sh
|
|
81348
|
+
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /etc/profile.d/bun.sh
|
|
81349
|
+
`;
|
|
81350
|
+
} else if (runtime === "node") {
|
|
81351
|
+
const nodeMajor = runtimeVersion === "latest" || !runtimeVersion ? "20" : runtimeVersion.split(".")[0];
|
|
81352
|
+
script += `
|
|
81353
|
+
curl -fsSL https://deb.nodesource.com/setup_${nodeMajor}.x | bash -
|
|
81354
|
+
apt-get install -y nodejs
|
|
81355
|
+
ln -sf /usr/bin/node /usr/local/bin/node
|
|
81356
|
+
ln -sf /usr/bin/npm /usr/local/bin/npm
|
|
81357
|
+
`;
|
|
81358
|
+
} else if (runtime === "deno") {
|
|
81359
|
+
script += `
|
|
81360
|
+
curl -fsSL https://deno.land/install.sh | sh
|
|
81361
|
+
ln -sf /root/.deno/bin/deno /usr/local/bin/deno
|
|
81362
|
+
`;
|
|
81363
|
+
}
|
|
81364
|
+
script += `
|
|
81365
|
+
mkdir -p /var/www /var/ts-cloud/staging /var/ts-cloud/releases
|
|
81366
|
+
`;
|
|
81367
|
+
if (caddyfile) {
|
|
81368
|
+
const escaped = caddyfile.replace(/\$/g, "\\$");
|
|
81369
|
+
script += `
|
|
81370
|
+
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
|
81371
|
+
curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=\${ARCH}" -o /usr/local/bin/caddy
|
|
81372
|
+
chmod +x /usr/local/bin/caddy
|
|
81373
|
+
|
|
81374
|
+
getent group caddy >/dev/null || groupadd --system caddy
|
|
81375
|
+
getent passwd caddy >/dev/null || useradd --system --gid caddy \\
|
|
81376
|
+
--create-home --home-dir /var/lib/caddy \\
|
|
81377
|
+
--shell /usr/sbin/nologin --comment "Caddy web server" caddy
|
|
81378
|
+
|
|
81379
|
+
mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
|
|
81380
|
+
chown -R caddy:caddy /var/lib/caddy /var/log/caddy
|
|
81381
|
+
|
|
81382
|
+
cat > /etc/systemd/system/caddy.service <<'CADDY_UNIT_EOF'
|
|
81383
|
+
[Unit]
|
|
81384
|
+
Description=Caddy
|
|
81385
|
+
Documentation=https://caddyserver.com/docs/
|
|
81386
|
+
After=network.target network-online.target
|
|
81387
|
+
Requires=network-online.target
|
|
81388
|
+
|
|
81389
|
+
[Service]
|
|
81390
|
+
Type=notify
|
|
81391
|
+
User=caddy
|
|
81392
|
+
Group=caddy
|
|
81393
|
+
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
|
|
81394
|
+
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
|
|
81395
|
+
TimeoutStopSec=5s
|
|
81396
|
+
LimitNOFILE=1048576
|
|
81397
|
+
PrivateTmp=true
|
|
81398
|
+
ProtectSystem=full
|
|
81399
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
81400
|
+
|
|
81401
|
+
[Install]
|
|
81402
|
+
WantedBy=multi-user.target
|
|
81403
|
+
CADDY_UNIT_EOF
|
|
81404
|
+
|
|
81405
|
+
cat > /etc/caddy/Caddyfile <<'CADDY_CONFIG_EOF'
|
|
81406
|
+
${escaped}
|
|
81407
|
+
CADDY_CONFIG_EOF
|
|
81408
|
+
|
|
81409
|
+
systemctl daemon-reload
|
|
81410
|
+
systemctl enable caddy
|
|
81411
|
+
systemctl start caddy
|
|
81412
|
+
`;
|
|
81413
|
+
}
|
|
81414
|
+
script += `
|
|
81415
|
+
echo "ts-cloud bootstrap complete — instance is ready for site deploys"
|
|
81416
|
+
`;
|
|
81417
|
+
return script;
|
|
81418
|
+
}
|
|
81419
|
+
function wrapCloudInitUserData(bootstrapScript) {
|
|
81420
|
+
return `#cloud-config
|
|
81421
|
+
runcmd:
|
|
81422
|
+
- |
|
|
81423
|
+
${bootstrapScript.split(`
|
|
81424
|
+
`).join(`
|
|
81425
|
+
`)}
|
|
81426
|
+
`;
|
|
81427
|
+
}
|
|
81428
|
+
|
|
81429
|
+
// src/drivers/hetzner/firewall-rules.ts
|
|
81430
|
+
function buildHetznerFirewallRules(config6) {
|
|
81431
|
+
const openPorts = new Set([80, 443, ...config6.sitePorts]);
|
|
81432
|
+
if (config6.allowSsh) {
|
|
81433
|
+
openPorts.add(22);
|
|
81434
|
+
}
|
|
81435
|
+
return [...openPorts].map((port) => {
|
|
81436
|
+
return {
|
|
81437
|
+
direction: "in",
|
|
81438
|
+
protocol: "tcp",
|
|
81439
|
+
port: String(port),
|
|
81440
|
+
source_ips: ["0.0.0.0/0", "::/0"],
|
|
81441
|
+
description: `ts-cloud port ${port}`
|
|
81442
|
+
};
|
|
81443
|
+
});
|
|
81444
|
+
}
|
|
81445
|
+
|
|
81446
|
+
// src/drivers/hetzner/instance-sizes.ts
|
|
81447
|
+
var HETZNER_INSTANCE_TYPES = {
|
|
81448
|
+
micro: "cpx11",
|
|
81449
|
+
small: "cx22",
|
|
81450
|
+
medium: "cx32",
|
|
81451
|
+
large: "cx42",
|
|
81452
|
+
xlarge: "cx52",
|
|
81453
|
+
"2xlarge": "ccx33"
|
|
81454
|
+
};
|
|
81455
|
+
function resolveHetznerServerType(size) {
|
|
81456
|
+
if (!size)
|
|
81457
|
+
return HETZNER_INSTANCE_TYPES.micro;
|
|
81458
|
+
if (size in HETZNER_INSTANCE_TYPES) {
|
|
81459
|
+
return HETZNER_INSTANCE_TYPES[size];
|
|
81460
|
+
}
|
|
81461
|
+
return size;
|
|
81462
|
+
}
|
|
81463
|
+
var TS_CLOUD_LABEL_PREFIX = "ts-cloud";
|
|
81464
|
+
function tsCloudLabels(slug, environment, role = "app") {
|
|
81465
|
+
return {
|
|
81466
|
+
[`${TS_CLOUD_LABEL_PREFIX}/project`]: slug,
|
|
81467
|
+
[`${TS_CLOUD_LABEL_PREFIX}/environment`]: environment,
|
|
81468
|
+
[`${TS_CLOUD_LABEL_PREFIX}/role`]: role,
|
|
81469
|
+
[`${TS_CLOUD_LABEL_PREFIX}/managed-by`]: "ts-cloud"
|
|
81470
|
+
};
|
|
81471
|
+
}
|
|
81472
|
+
function matchesTsCloudLabels(labels, slug, environment, role = "app") {
|
|
81473
|
+
if (!labels)
|
|
81474
|
+
return false;
|
|
81475
|
+
return labels[`${TS_CLOUD_LABEL_PREFIX}/project`] === slug && labels[`${TS_CLOUD_LABEL_PREFIX}/environment`] === environment && labels[`${TS_CLOUD_LABEL_PREFIX}/role`] === role;
|
|
81476
|
+
}
|
|
81477
|
+
|
|
81478
|
+
// src/drivers/hetzner/state.ts
|
|
81479
|
+
import { mkdir as mkdir4, readFile, writeFile as writeFile4 } from "node:fs/promises";
|
|
81480
|
+
import { join as join12 } from "node:path";
|
|
81481
|
+
var STATE_DIR = ".ts-cloud/state";
|
|
81482
|
+
function driverStatePath(stackName) {
|
|
81483
|
+
return join12(process.cwd(), STATE_DIR, `${stackName}.json`);
|
|
81484
|
+
}
|
|
81485
|
+
async function readDriverState(stackName) {
|
|
81486
|
+
try {
|
|
81487
|
+
const raw = await readFile(driverStatePath(stackName), "utf8");
|
|
81488
|
+
return JSON.parse(raw);
|
|
81489
|
+
} catch {
|
|
81490
|
+
return null;
|
|
81491
|
+
}
|
|
81492
|
+
}
|
|
81493
|
+
async function writeDriverState(stackName, state) {
|
|
81494
|
+
const path = driverStatePath(stackName);
|
|
81495
|
+
await mkdir4(join12(process.cwd(), STATE_DIR), { recursive: true });
|
|
81496
|
+
await writeFile4(path, `${JSON.stringify(state, null, 2)}
|
|
81497
|
+
`, "utf8");
|
|
81498
|
+
}
|
|
81499
|
+
|
|
81500
|
+
// src/drivers/hetzner/driver.ts
|
|
81501
|
+
function expandHome(path) {
|
|
81502
|
+
return path.startsWith("~/") ? join13(homedir7(), path.slice(2)) : path;
|
|
81503
|
+
}
|
|
81504
|
+
|
|
81505
|
+
class HetznerDriver {
|
|
81506
|
+
name = "hetzner";
|
|
81507
|
+
usesCloudFormation = false;
|
|
81508
|
+
client;
|
|
81509
|
+
sshPrivateKeyPath;
|
|
81510
|
+
sshUser;
|
|
81511
|
+
location;
|
|
81512
|
+
constructor(options = {}) {
|
|
81513
|
+
this.client = options.client ?? new HetznerClient({
|
|
81514
|
+
apiToken: resolveHetznerApiToken(options.apiToken)
|
|
81515
|
+
});
|
|
81516
|
+
this.sshPrivateKeyPath = expandHome(options.sshPrivateKeyPath || process.env.HCLOUD_SSH_KEY || "~/.ssh/id_ed25519");
|
|
81517
|
+
this.sshUser = options.sshUser || process.env.HCLOUD_SSH_USER || "root";
|
|
81518
|
+
this.location = options.location || process.env.HCLOUD_LOCATION || "fsn1";
|
|
81519
|
+
}
|
|
81520
|
+
async provisionComputeInfrastructure(options) {
|
|
81521
|
+
const { config: config6, environment } = options;
|
|
81522
|
+
const slug = config6.project.slug;
|
|
81523
|
+
const compute = config6.infrastructure?.compute;
|
|
81524
|
+
if (!compute) {
|
|
81525
|
+
throw new Error("infrastructure.compute is required to provision Hetzner compute");
|
|
81526
|
+
}
|
|
81527
|
+
const stackName = resolveProjectStackName(config6, environment);
|
|
81528
|
+
const existing = await readDriverState(stackName);
|
|
81529
|
+
if (existing?.serverId) {
|
|
81530
|
+
const server2 = await this.client.getServer(existing.serverId);
|
|
81531
|
+
if (server2.status !== "off") {
|
|
81532
|
+
return this.outputsFromState(existing, server2);
|
|
81533
|
+
}
|
|
81534
|
+
}
|
|
81535
|
+
const sites = config6.sites || {};
|
|
81536
|
+
const sitePorts = Object.values(sites).map((site) => site.port).filter((port) => typeof port === "number" && ![80, 443].includes(port));
|
|
81537
|
+
const caddyfile = buildCaddyfile(sites);
|
|
81538
|
+
const bootstrap = generateUbuntuAppCloudInit({
|
|
81539
|
+
runtime: compute.runtime || "bun",
|
|
81540
|
+
runtimeVersion: compute.runtimeVersion || "latest",
|
|
81541
|
+
systemPackages: compute.systemPackages,
|
|
81542
|
+
database: config6.infrastructure?.database,
|
|
81543
|
+
caddyfile
|
|
81544
|
+
});
|
|
81545
|
+
const userData = wrapCloudInitUserData(bootstrap);
|
|
81546
|
+
const serverName = `${slug}-${environment}-app`;
|
|
81547
|
+
const serverType = resolveHetznerServerType(compute.size);
|
|
81548
|
+
const image = compute.image || config6.hetzner?.image || "ubuntu-24.04";
|
|
81549
|
+
const labels = tsCloudLabels(slug, environment, "app");
|
|
81550
|
+
const firewallName = `${slug}-${environment}-app-fw`;
|
|
81551
|
+
const { firewall } = await this.client.createFirewall({
|
|
81552
|
+
name: firewallName,
|
|
81553
|
+
labels,
|
|
81554
|
+
rules: buildHetznerFirewallRules({
|
|
81555
|
+
allowSsh: compute.allowSsh,
|
|
81556
|
+
sitePorts
|
|
81557
|
+
})
|
|
81558
|
+
});
|
|
81559
|
+
const { server, action } = await this.client.createServer({
|
|
81560
|
+
name: serverName,
|
|
81561
|
+
serverType,
|
|
81562
|
+
image,
|
|
81563
|
+
location: config6.hetzner?.location || this.location,
|
|
81564
|
+
userData,
|
|
81565
|
+
labels,
|
|
81566
|
+
firewalls: [{ firewall: firewall.id }]
|
|
81567
|
+
});
|
|
81568
|
+
await this.client.waitForAction(action.id);
|
|
81569
|
+
const running = await this.client.waitForServerRunning(server.id);
|
|
81570
|
+
const state = {
|
|
81571
|
+
provider: "hetzner",
|
|
81572
|
+
stackName,
|
|
81573
|
+
serverId: running.id,
|
|
81574
|
+
serverName: running.name,
|
|
81575
|
+
firewallId: firewall.id,
|
|
81576
|
+
publicIp: running.public_net.ipv4?.ip,
|
|
81577
|
+
deployStoragePath: "/var/ts-cloud/staging",
|
|
81578
|
+
sshUser: this.sshUser
|
|
81579
|
+
};
|
|
81580
|
+
await writeDriverState(stackName, state);
|
|
81581
|
+
return this.outputsFromState(state, running);
|
|
81582
|
+
}
|
|
81583
|
+
async getComputeOutputs(options) {
|
|
81584
|
+
const stackName = resolveProjectStackName(options.config, options.environment);
|
|
81585
|
+
const state = await readDriverState(stackName);
|
|
81586
|
+
if (state?.serverId) {
|
|
81587
|
+
const server = await this.client.getServer(state.serverId);
|
|
81588
|
+
return this.outputsFromState(state, server);
|
|
81589
|
+
}
|
|
81590
|
+
const targets = await this.findComputeTargets({
|
|
81591
|
+
slug: options.config.project.slug,
|
|
81592
|
+
environment: options.environment,
|
|
81593
|
+
role: "app"
|
|
81594
|
+
});
|
|
81595
|
+
const first = targets[0];
|
|
81596
|
+
return {
|
|
81597
|
+
deployStoragePath: "/var/ts-cloud/staging",
|
|
81598
|
+
appInstanceId: first?.id,
|
|
81599
|
+
appPublicIp: first?.publicIp,
|
|
81600
|
+
sshUser: this.sshUser
|
|
81601
|
+
};
|
|
81602
|
+
}
|
|
81603
|
+
async uploadRelease(options) {
|
|
81604
|
+
const targets = options.targets?.length ? options.targets : await this.findComputeTargets({
|
|
81605
|
+
slug: options.config.project.slug,
|
|
81606
|
+
environment: options.environment,
|
|
81607
|
+
role: "app"
|
|
81608
|
+
});
|
|
81609
|
+
if (targets.length === 0) {
|
|
81610
|
+
throw new Error("No Hetzner compute targets found for release upload");
|
|
81611
|
+
}
|
|
81612
|
+
const remotePath = `/var/ts-cloud/staging/${options.remoteKey.split("/").pop()}`;
|
|
81613
|
+
for (const target of targets) {
|
|
81614
|
+
if (!target.publicIp) {
|
|
81615
|
+
throw new Error(`Target ${target.id} has no public IP for SCP upload`);
|
|
81616
|
+
}
|
|
81617
|
+
this.scpToHost(target.publicIp, options.localPath, remotePath);
|
|
81618
|
+
}
|
|
81619
|
+
return { artifactRef: remotePath };
|
|
81620
|
+
}
|
|
81621
|
+
async findComputeTargets(options) {
|
|
81622
|
+
const servers = await this.client.listServers();
|
|
81623
|
+
return servers.filter((server) => matchesTsCloudLabels(server.labels, options.slug, options.environment, options.role || "app")).map((server) => ({
|
|
81624
|
+
id: String(server.id),
|
|
81625
|
+
name: server.name,
|
|
81626
|
+
publicIp: server.public_net.ipv4?.ip,
|
|
81627
|
+
privateIp: server.private_net?.[0]?.ip,
|
|
81628
|
+
status: server.status
|
|
81629
|
+
}));
|
|
81630
|
+
}
|
|
81631
|
+
async runRemoteDeploy(options) {
|
|
81632
|
+
if (options.targets.length === 0) {
|
|
81633
|
+
return { success: false, instanceCount: 0, perInstance: [], error: "No targets provided" };
|
|
81634
|
+
}
|
|
81635
|
+
const script = options.commands.join(`
|
|
81636
|
+
`);
|
|
81637
|
+
const perInstance = [];
|
|
81638
|
+
for (const target of options.targets) {
|
|
81639
|
+
if (!target.publicIp) {
|
|
81640
|
+
perInstance.push({
|
|
81641
|
+
instanceId: target.id,
|
|
81642
|
+
status: "Failed",
|
|
81643
|
+
error: "Missing public IP"
|
|
81644
|
+
});
|
|
81645
|
+
continue;
|
|
81646
|
+
}
|
|
81647
|
+
try {
|
|
81648
|
+
const output = this.sshExec(target.publicIp, script);
|
|
81649
|
+
perInstance.push({
|
|
81650
|
+
instanceId: target.id,
|
|
81651
|
+
status: "Success",
|
|
81652
|
+
output
|
|
81653
|
+
});
|
|
81654
|
+
} catch (err) {
|
|
81655
|
+
perInstance.push({
|
|
81656
|
+
instanceId: target.id,
|
|
81657
|
+
status: "Failed",
|
|
81658
|
+
error: err.message
|
|
81659
|
+
});
|
|
81660
|
+
}
|
|
81661
|
+
}
|
|
81662
|
+
const success = perInstance.every((r) => r.status === "Success");
|
|
81663
|
+
return {
|
|
81664
|
+
success,
|
|
81665
|
+
instanceCount: options.targets.length,
|
|
81666
|
+
perInstance,
|
|
81667
|
+
error: success ? undefined : "One or more SSH deploy commands failed"
|
|
81668
|
+
};
|
|
81669
|
+
}
|
|
81670
|
+
outputsFromState(state, server) {
|
|
81671
|
+
return {
|
|
81672
|
+
deployStoragePath: state.deployStoragePath || "/var/ts-cloud/staging",
|
|
81673
|
+
appInstanceId: String(state.serverId),
|
|
81674
|
+
appPublicIp: server?.public_net.ipv4?.ip || state.publicIp,
|
|
81675
|
+
sshUser: state.sshUser || this.sshUser
|
|
81676
|
+
};
|
|
81677
|
+
}
|
|
81678
|
+
sshBaseArgs(host) {
|
|
81679
|
+
return [
|
|
81680
|
+
"-i",
|
|
81681
|
+
this.sshPrivateKeyPath,
|
|
81682
|
+
"-o",
|
|
81683
|
+
"StrictHostKeyChecking=accept-new",
|
|
81684
|
+
"-o",
|
|
81685
|
+
"BatchMode=yes",
|
|
81686
|
+
`${this.sshUser}@${host}`
|
|
81687
|
+
];
|
|
81688
|
+
}
|
|
81689
|
+
scpToHost(host, localPath, remotePath) {
|
|
81690
|
+
execSync([
|
|
81691
|
+
"scp",
|
|
81692
|
+
"-i",
|
|
81693
|
+
this.sshPrivateKeyPath,
|
|
81694
|
+
"-o",
|
|
81695
|
+
"StrictHostKeyChecking=accept-new",
|
|
81696
|
+
"-o",
|
|
81697
|
+
"BatchMode=yes",
|
|
81698
|
+
localPath,
|
|
81699
|
+
`${this.sshUser}@${host}:${remotePath}`
|
|
81700
|
+
].map((arg) => `"${arg.replace(/"/g, "\\\"")}"`).join(" "), { stdio: "pipe" });
|
|
81701
|
+
}
|
|
81702
|
+
sshExec(host, script) {
|
|
81703
|
+
const escaped = script.replace(/'/g, `'\\''`);
|
|
81704
|
+
return execSync(`ssh ${this.sshBaseArgs(host).map((a) => `"${a.replace(/"/g, "\\\"")}"`).join(" ")} '${escaped}'`, {
|
|
81705
|
+
encoding: "utf8",
|
|
81706
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
81707
|
+
});
|
|
81708
|
+
}
|
|
81709
|
+
}
|
|
81710
|
+
|
|
81711
|
+
// src/drivers/factory.ts
|
|
81712
|
+
function createCloudDriver(options) {
|
|
81713
|
+
const provider = options.provider ?? resolveCloudProvider(options.config);
|
|
81714
|
+
switch (provider) {
|
|
81715
|
+
case "aws":
|
|
81716
|
+
return new AwsDriver({ region: options.config.project.region });
|
|
81717
|
+
case "hetzner":
|
|
81718
|
+
return new HetznerDriver({
|
|
81719
|
+
apiToken: options.config.hetzner?.apiToken,
|
|
81720
|
+
sshPrivateKeyPath: options.config.hetzner?.sshPrivateKeyPath,
|
|
81721
|
+
sshUser: options.config.hetzner?.sshUser,
|
|
81722
|
+
location: options.config.hetzner?.location
|
|
81723
|
+
});
|
|
81724
|
+
default:
|
|
81725
|
+
throw new Error(`Unknown cloud provider: ${options.provider ?? resolveCloudProvider(options.config)}`);
|
|
81726
|
+
}
|
|
81727
|
+
}
|
|
81728
|
+
|
|
81729
|
+
class CloudDriverFactory {
|
|
81730
|
+
drivers = new Map;
|
|
81731
|
+
getDriver(config6, provider) {
|
|
81732
|
+
const name = provider ?? resolveCloudProvider(config6);
|
|
81733
|
+
const cacheKey = `${name}:${config6.project.slug}:${config6.project.region || "default"}`;
|
|
81734
|
+
const cached = this.drivers.get(cacheKey);
|
|
81735
|
+
if (cached)
|
|
81736
|
+
return cached;
|
|
81737
|
+
const driver = createCloudDriver({ config: config6, provider: name });
|
|
81738
|
+
this.drivers.set(cacheKey, driver);
|
|
81739
|
+
return driver;
|
|
81740
|
+
}
|
|
81741
|
+
}
|
|
81742
|
+
var cloudDrivers = new CloudDriverFactory;
|
|
81743
|
+
// src/drivers/shared/deploy-script.ts
|
|
81744
|
+
function resolveExecStart(start, runtime) {
|
|
81745
|
+
const bin = runtime === "bun" ? "/usr/local/bin/bun" : runtime === "deno" ? "/usr/local/bin/deno" : "/usr/local/bin/node";
|
|
81746
|
+
const args = start.replace(/^(bun|node|deno)\s+/, "");
|
|
81747
|
+
return `${bin} ${args}`;
|
|
81748
|
+
}
|
|
81749
|
+
function buildSiteDeployScript(options) {
|
|
81750
|
+
const {
|
|
81751
|
+
siteName,
|
|
81752
|
+
slug,
|
|
81753
|
+
artifactFetch,
|
|
81754
|
+
execStart,
|
|
81755
|
+
envEntries,
|
|
81756
|
+
port
|
|
81757
|
+
} = options;
|
|
81758
|
+
const appDir = options.appDir ?? `/var/www/${siteName}`;
|
|
81759
|
+
const serviceName = `${slug}-${siteName}.service`;
|
|
81760
|
+
const envFile = Object.entries(envEntries).map(([k, v]) => `${k}=${JSON.stringify(String(v))}`).join(`
|
|
81761
|
+
`);
|
|
81762
|
+
return [
|
|
81763
|
+
"set -euo pipefail",
|
|
81764
|
+
...artifactFetch,
|
|
81765
|
+
`mkdir -p ${appDir}`,
|
|
81766
|
+
`find ${appDir} -mindepth 1 -maxdepth 1 ! -name '.env' -exec rm -rf {} +`,
|
|
81767
|
+
`tar xzf /tmp/${siteName}-release.tar.gz -C ${appDir}`,
|
|
81768
|
+
`cat > ${appDir}/.env <<'TS_CLOUD_ENV_EOF'`,
|
|
81769
|
+
envFile,
|
|
81770
|
+
"TS_CLOUD_ENV_EOF",
|
|
81771
|
+
`chmod 600 ${appDir}/.env`,
|
|
81772
|
+
`cat > /etc/systemd/system/${serviceName} <<'TS_CLOUD_UNIT_EOF'`,
|
|
81773
|
+
"[Unit]",
|
|
81774
|
+
`Description=${siteName} (managed by ts-cloud)`,
|
|
81775
|
+
"After=network.target",
|
|
81776
|
+
"",
|
|
81777
|
+
"[Service]",
|
|
81778
|
+
"Type=simple",
|
|
81779
|
+
`WorkingDirectory=${appDir}`,
|
|
81780
|
+
`ExecStart=${execStart}`,
|
|
81781
|
+
"Restart=always",
|
|
81782
|
+
"RestartSec=5",
|
|
81783
|
+
`EnvironmentFile=${appDir}/.env`,
|
|
81784
|
+
...port ? [`Environment=PORT=${port}`] : [],
|
|
81785
|
+
"",
|
|
81786
|
+
"[Install]",
|
|
81787
|
+
"WantedBy=multi-user.target",
|
|
81788
|
+
"TS_CLOUD_UNIT_EOF",
|
|
81789
|
+
"systemctl daemon-reload",
|
|
81790
|
+
`systemctl enable ${serviceName}`,
|
|
81791
|
+
`systemctl restart ${serviceName}`,
|
|
81792
|
+
`systemctl is-active ${serviceName}`
|
|
81793
|
+
];
|
|
81794
|
+
}
|
|
81795
|
+
function buildAwsArtifactFetch(bucket, key, region, siteName) {
|
|
81796
|
+
return [
|
|
81797
|
+
`aws s3 cp "s3://${bucket}/${key}" /tmp/${siteName}-release.tar.gz --region ${region}`
|
|
81798
|
+
];
|
|
81799
|
+
}
|
|
81800
|
+
function buildLocalArtifactFetch(localPath, siteName) {
|
|
81801
|
+
return [
|
|
81802
|
+
`cp "${localPath}" /tmp/${siteName}-release.tar.gz`
|
|
81803
|
+
];
|
|
81804
|
+
}
|
|
81805
|
+
// src/drivers/shared/compute-deploy.ts
|
|
81806
|
+
var noopLogger = {
|
|
81807
|
+
info: () => {},
|
|
81808
|
+
warn: () => {},
|
|
81809
|
+
error: () => {},
|
|
81810
|
+
step: () => {},
|
|
81811
|
+
success: () => {}
|
|
81812
|
+
};
|
|
81813
|
+
async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
81814
|
+
const {
|
|
81815
|
+
config: config6,
|
|
81816
|
+
environment,
|
|
81817
|
+
siteName,
|
|
81818
|
+
site,
|
|
81819
|
+
slug,
|
|
81820
|
+
sha,
|
|
81821
|
+
runtime,
|
|
81822
|
+
localTarballPath
|
|
81823
|
+
} = options;
|
|
81824
|
+
const remoteKey = `releases/${siteName}/${sha}.tar.gz`;
|
|
81825
|
+
const stackName = resolveProjectStackName(config6, environment);
|
|
81826
|
+
const outputs = await driver.getComputeOutputs({ config: config6, environment });
|
|
81827
|
+
const targets = await driver.findComputeTargets({
|
|
81828
|
+
slug,
|
|
81829
|
+
environment,
|
|
81830
|
+
role: "app"
|
|
81831
|
+
});
|
|
81832
|
+
if (targets.length === 0) {
|
|
81833
|
+
const hint = driver.name === "aws" ? `Stack '${stackName}' has no EC2 instances tagged Project=${slug} Environment=${environment} Role=app.` : `No Hetzner servers labeled ts-cloud/project=${slug} ts-cloud/environment=${environment} ts-cloud/role=app.`;
|
|
81834
|
+
return { success: false, error: hint };
|
|
81835
|
+
}
|
|
81836
|
+
const uploadResult = await driver.uploadRelease({
|
|
81837
|
+
config: config6,
|
|
81838
|
+
environment,
|
|
81839
|
+
localPath: localTarballPath,
|
|
81840
|
+
remoteKey,
|
|
81841
|
+
targets
|
|
81842
|
+
});
|
|
81843
|
+
const artifactFetch = driver.name === "aws" ? buildAwsArtifactFetch(outputs.deployBucketName, remoteKey, config6.project.region || "us-east-1", siteName) : buildLocalArtifactFetch(uploadResult.artifactRef, siteName);
|
|
81844
|
+
const remoteScript = buildSiteDeployScript({
|
|
81845
|
+
siteName,
|
|
81846
|
+
slug,
|
|
81847
|
+
artifactFetch,
|
|
81848
|
+
execStart: resolveExecStart(site.start, runtime),
|
|
81849
|
+
envEntries: site.env || {},
|
|
81850
|
+
port: site.port
|
|
81851
|
+
});
|
|
81852
|
+
logger4.step(`Deploying to ${targets.length} target(s)...`);
|
|
81853
|
+
const result = await driver.runRemoteDeploy({
|
|
81854
|
+
targets,
|
|
81855
|
+
commands: remoteScript,
|
|
81856
|
+
comment: `ts-cloud deploy ${slug}/${siteName}@${sha}`,
|
|
81857
|
+
tags: {
|
|
81858
|
+
Project: slug,
|
|
81859
|
+
Environment: environment,
|
|
81860
|
+
Role: "app"
|
|
81861
|
+
}
|
|
81862
|
+
});
|
|
81863
|
+
if (!result.success) {
|
|
81864
|
+
return {
|
|
81865
|
+
success: false,
|
|
81866
|
+
error: result.error || "Remote deploy failed",
|
|
81867
|
+
instanceCount: result.instanceCount,
|
|
81868
|
+
perInstance: result.perInstance
|
|
81869
|
+
};
|
|
81870
|
+
}
|
|
81871
|
+
return {
|
|
81872
|
+
success: true,
|
|
81873
|
+
instanceCount: result.instanceCount,
|
|
81874
|
+
perInstance: result.perInstance
|
|
81875
|
+
};
|
|
81876
|
+
}
|
|
81877
|
+
async function deployAllComputeSites(options) {
|
|
81878
|
+
const { config: config6, environment, driver, sha, runtime, tarballForSite, logger: logger4 = noopLogger } = options;
|
|
81879
|
+
const slug = config6.project.slug;
|
|
81880
|
+
const sites = config6.sites || {};
|
|
81881
|
+
const deployable = Object.entries(sites).filter(([name, site]) => {
|
|
81882
|
+
if (!site?.start) {
|
|
81883
|
+
logger4.warn(`Site '${name}' has no \`start\` command — skipping (compute mode requires every site to declare how to run).`);
|
|
81884
|
+
return false;
|
|
81885
|
+
}
|
|
81886
|
+
return true;
|
|
81887
|
+
});
|
|
81888
|
+
if (deployable.length === 0)
|
|
81889
|
+
return true;
|
|
81890
|
+
for (const [siteName, site] of deployable) {
|
|
81891
|
+
logger4.step(`Deploying site: ${siteName}`);
|
|
81892
|
+
const result = await deploySiteRelease(driver, {
|
|
81893
|
+
config: config6,
|
|
81894
|
+
environment,
|
|
81895
|
+
siteName,
|
|
81896
|
+
site,
|
|
81897
|
+
slug,
|
|
81898
|
+
sha,
|
|
81899
|
+
runtime,
|
|
81900
|
+
localTarballPath: tarballForSite(siteName)
|
|
81901
|
+
}, logger4);
|
|
81902
|
+
if (!result.success) {
|
|
81903
|
+
logger4.error(`Deploy of '${siteName}' failed: ${result.error || "unknown error"}`);
|
|
81904
|
+
if (result.perInstance) {
|
|
81905
|
+
for (const inst of result.perInstance) {
|
|
81906
|
+
logger4.error(` ${inst.instanceId}: ${inst.status}${inst.error ? ` — ${inst.error}` : ""}`);
|
|
81907
|
+
}
|
|
81908
|
+
}
|
|
81909
|
+
return false;
|
|
81910
|
+
}
|
|
81911
|
+
logger4.success(`Deployed ${slug}/${siteName}@${sha} to ${result.instanceCount} target(s)`);
|
|
81912
|
+
if (result.perInstance) {
|
|
81913
|
+
for (const inst of result.perInstance) {
|
|
81914
|
+
logger4.info(` ✓ ${inst.instanceId}: ${inst.status}`);
|
|
81915
|
+
}
|
|
81916
|
+
}
|
|
81917
|
+
}
|
|
81918
|
+
return true;
|
|
81919
|
+
}
|
|
80515
81920
|
// src/index.ts
|
|
80516
81921
|
init_dns();
|
|
80517
81922
|
export {
|
|
80518
81923
|
xrayManager,
|
|
81924
|
+
wrapCloudInitUserData,
|
|
80519
81925
|
withTimeout,
|
|
80520
81926
|
withSecurity,
|
|
80521
81927
|
withQueue,
|
|
@@ -80558,13 +81964,24 @@ export {
|
|
|
80558
81964
|
route53RoutingManager,
|
|
80559
81965
|
route53ResolverManager,
|
|
80560
81966
|
resourceManagementManager,
|
|
81967
|
+
resolveStorageBucketName,
|
|
81968
|
+
resolveSiteStackName,
|
|
81969
|
+
resolveSiteResourceName,
|
|
81970
|
+
resolveSiteBucketName,
|
|
80561
81971
|
resolveRegion,
|
|
81972
|
+
resolveProjectStackName,
|
|
81973
|
+
resolveObjectStorage,
|
|
81974
|
+
resolveHetznerApiToken,
|
|
81975
|
+
resolveExecStart,
|
|
81976
|
+
resolveDeployBucketName,
|
|
80562
81977
|
resolveCredentials,
|
|
81978
|
+
resolveCloudProvider,
|
|
80563
81979
|
requiresReplacement,
|
|
80564
81980
|
replicaManager,
|
|
80565
81981
|
regionPairManager,
|
|
80566
81982
|
quickHash,
|
|
80567
81983
|
queueManagementManager,
|
|
81984
|
+
providerEndpoint,
|
|
80568
81985
|
progressiveDeploymentManager,
|
|
80569
81986
|
processInChunks,
|
|
80570
81987
|
previewNotifications,
|
|
@@ -80634,6 +82051,7 @@ export {
|
|
|
80634
82051
|
getClosestRegion,
|
|
80635
82052
|
getAllRegions,
|
|
80636
82053
|
getAccountId,
|
|
82054
|
+
generateUbuntuAppCloudInit,
|
|
80637
82055
|
generateStaticSiteTemplate,
|
|
80638
82056
|
generateScheduledWorkflow,
|
|
80639
82057
|
generateScheduledPipeline,
|
|
@@ -80696,7 +82114,9 @@ export {
|
|
|
80696
82114
|
deployStaticSiteWithExternalDns,
|
|
80697
82115
|
deployStaticSiteFull,
|
|
80698
82116
|
deployStaticSite,
|
|
82117
|
+
deploySiteRelease,
|
|
80699
82118
|
deploySite,
|
|
82119
|
+
deployAllComputeSites,
|
|
80700
82120
|
deleteStaticSite,
|
|
80701
82121
|
defaultLocalConfig,
|
|
80702
82122
|
defaultConfig4 as defaultConfig,
|
|
@@ -80712,6 +82132,7 @@ export {
|
|
|
80712
82132
|
createPresignedUrl,
|
|
80713
82133
|
createPreset,
|
|
80714
82134
|
createPorkbunValidator,
|
|
82135
|
+
createObjectStorageClient,
|
|
80715
82136
|
createNodeJsServerlessPreset,
|
|
80716
82137
|
createNodeJsServerPreset,
|
|
80717
82138
|
createMockAWS,
|
|
@@ -80724,11 +82145,13 @@ export {
|
|
|
80724
82145
|
createDnsProvider,
|
|
80725
82146
|
createDataPipelinePreset,
|
|
80726
82147
|
createCredentialProvider,
|
|
82148
|
+
createCloudDriver,
|
|
80727
82149
|
createApiBackendPreset,
|
|
80728
82150
|
containerRegistryManager,
|
|
80729
82151
|
config5 as config,
|
|
80730
82152
|
composePresets,
|
|
80731
82153
|
cloudTrailManager,
|
|
82154
|
+
cloudDrivers,
|
|
80732
82155
|
cloud_config_schema_default as cloudConfigSchema,
|
|
80733
82156
|
clearSigningKeyCache,
|
|
80734
82157
|
cleanupDns01Challenge,
|
|
@@ -80738,8 +82161,10 @@ export {
|
|
|
80738
82161
|
certificateManager,
|
|
80739
82162
|
categorizeChanges,
|
|
80740
82163
|
canaryManager,
|
|
82164
|
+
buildSiteDeployScript,
|
|
80741
82165
|
buildOptimizationManager,
|
|
80742
82166
|
buildCloudFormationTemplate,
|
|
82167
|
+
buildCaddyfile,
|
|
80743
82168
|
bounceComplaintHandler,
|
|
80744
82169
|
blueGreenManager,
|
|
80745
82170
|
batchProcessingManager,
|
|
@@ -80853,6 +82278,8 @@ export {
|
|
|
80853
82278
|
InfrastructureGenerator,
|
|
80854
82279
|
ImageScanningManager,
|
|
80855
82280
|
IAMClient,
|
|
82281
|
+
HetznerDriver,
|
|
82282
|
+
HetznerClient,
|
|
80856
82283
|
HealthCheckManager,
|
|
80857
82284
|
HashCache,
|
|
80858
82285
|
GuardDutyManager,
|
|
@@ -80904,6 +82331,7 @@ export {
|
|
|
80904
82331
|
CloudFormationClient,
|
|
80905
82332
|
CloudFormationBuilder,
|
|
80906
82333
|
CloudError,
|
|
82334
|
+
CloudDriverFactory,
|
|
80907
82335
|
CertificateManager,
|
|
80908
82336
|
CanaryManager,
|
|
80909
82337
|
Cache,
|
|
@@ -80916,6 +82344,7 @@ export {
|
|
|
80916
82344
|
BedrockClient,
|
|
80917
82345
|
BatchProcessingManager,
|
|
80918
82346
|
BackupManager,
|
|
82347
|
+
AwsDriver,
|
|
80919
82348
|
Auth,
|
|
80920
82349
|
AssetHasher,
|
|
80921
82350
|
Arn,
|