@stacksjs/ts-cloud 0.5.0 → 0.5.1
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.
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* For the `custom` provider the vhost is rendered with the operator's cert
|
|
11
11
|
* directly (see nginx-vhost's `ssl` option), so certbot is not involved.
|
|
12
12
|
*/
|
|
13
|
-
import type { SiteConfig } from '@ts-cloud/core';
|
|
13
|
+
import type { SiteConfig, SslDnsConfig } from '@ts-cloud/core';
|
|
14
14
|
/** Resolve the effective SSL provider for a site (Let's Encrypt by default when it has a domain). */
|
|
15
15
|
export declare function resolveSslProvider(site: SiteConfig): 'letsencrypt' | 'custom' | 'none';
|
|
16
|
-
/**
|
|
17
|
-
export declare function
|
|
16
|
+
/** Path of the certbot credentials INI for a DNS provider. */
|
|
17
|
+
export declare function dnsCredentialsPath(provider: SslDnsConfig['provider']): string;
|
|
18
|
+
/** Install certbot (+ optional DNS plugin) and ensure the auto-renew timer is enabled. */
|
|
19
|
+
export declare function buildCertbotInstallScript(dns?: SslDnsConfig): string[];
|
|
18
20
|
export interface CertbotIssueOptions {
|
|
19
21
|
/** Primary domain. */
|
|
20
22
|
domain: string;
|
|
@@ -24,15 +26,27 @@ export interface CertbotIssueOptions {
|
|
|
24
26
|
email?: string;
|
|
25
27
|
/** Redirect HTTP → HTTPS (Forge default). @default true */
|
|
26
28
|
redirect?: boolean;
|
|
29
|
+
/** Issue `*.<domain>` + `<domain>` (requires `dns`). */
|
|
30
|
+
wildcard?: boolean;
|
|
31
|
+
/** DNS-01 validation via a certbot DNS plugin (required for wildcard). */
|
|
32
|
+
dns?: SslDnsConfig;
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
35
|
+
* Build the commands that write a DNS provider's certbot credentials INI
|
|
36
|
+
* (root-only) so certbot can create the `_acme-challenge` records. Returns `[]`
|
|
37
|
+
* when there are no credentials (e.g. route53 using instance-role creds).
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildDnsCredentialsScript(dns: SslDnsConfig): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Issue (or expand) a Let's Encrypt cert. Uses the nginx (HTTP-01) plugin by
|
|
42
|
+
* default; with `dns` set, uses DNS-01 via the provider plugin (the only way to
|
|
43
|
+
* get a wildcard). Idempotent: `--keep-until-expiring` reuses a valid cert.
|
|
32
44
|
*/
|
|
33
45
|
export declare function buildCertbotIssueScript(options: CertbotIssueOptions): string[];
|
|
34
46
|
/**
|
|
35
47
|
* Full SSL script for a site, dispatched on its provider. Returns `[]` for
|
|
36
|
-
* `custom`/`none` (custom certs are baked into the vhost already).
|
|
48
|
+
* `custom`/`none` (custom certs are baked into the vhost already). A wildcard
|
|
49
|
+
* request without a DNS config is impossible (HTTP-01 can't do wildcards), so
|
|
50
|
+
* it's skipped.
|
|
37
51
|
*/
|
|
38
52
|
export declare function buildSslScript(site: SiteConfig): string[];
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* (not-yet-live) release, so a failure leaves the previous release serving —
|
|
15
15
|
* the Envoyer zero-downtime guarantee.
|
|
16
16
|
*/
|
|
17
|
-
import type { SiteConfig } from '@ts-cloud/core';
|
|
17
|
+
import type { SiteConfig, SiteCredentialsConfig } from '@ts-cloud/core';
|
|
18
18
|
export declare const MACRO_CREATE_RELEASE = "$CREATE_RELEASE";
|
|
19
19
|
export declare const MACRO_ACTIVATE_RELEASE = "$ACTIVATE_RELEASE";
|
|
20
20
|
export declare const MACRO_RESTART_QUEUES = "$RESTART_QUEUES";
|
|
@@ -35,6 +35,11 @@ export interface LaravelDeployOptions {
|
|
|
35
35
|
/** PHP version selecting the `phpX.Y` binary. @default `site.phpVersion` ?? '8.3' */
|
|
36
36
|
defaultPhpVersion?: string;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Write private-registry credential files into the release before build:
|
|
40
|
+
* Composer `auth.json` and/or `.npmrc`. Quoted heredocs keep tokens literal.
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildCredentialFiles(releaseDir: string, creds?: SiteCredentialsConfig): string[];
|
|
38
43
|
/**
|
|
39
44
|
* Build the full remote shell script for a PHP/Laravel git deploy, expanding the
|
|
40
45
|
* release macros. Requires `site.repository` to be set.
|
|
@@ -45,6 +45,17 @@ export declare function buildLinkSharedPaths(paths: ReleasePaths, sharedPaths?:
|
|
|
45
45
|
* `mv -T`s it over `current` so there is no window where `current` is missing.
|
|
46
46
|
*/
|
|
47
47
|
export declare function buildActivateRelease(paths: ReleasePaths): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Roll the active release back to a previous one (Forge-style rollback). With
|
|
50
|
+
* `to` set, points `current` at `releases/<to>`; otherwise picks the most recent
|
|
51
|
+
* release that isn't the one `current` resolves to. Atomic (temp symlink + `mv
|
|
52
|
+
* -T`), and a no-op-safe guard fails loudly if the target is missing rather than
|
|
53
|
+
* leaving `current` dangling. The caller appends the engine reload
|
|
54
|
+
* (php-fpm/queues) — see {@link import('./laravel-deploy')}.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildRollbackScript(paths: ReleasePaths, options?: {
|
|
57
|
+
to?: string;
|
|
58
|
+
}): string[];
|
|
48
59
|
/**
|
|
49
60
|
* Remove all but the newest `keep` releases (by mtime). `current` always points
|
|
50
61
|
* at the newest, so it is never pruned.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server "recipes" — reusable bash scripts run on demand across one or more
|
|
3
|
+
* provisioned servers, as a chosen user (Forge's Recipes feature). The driver
|
|
4
|
+
* fans a recipe out over the target servers (SSH/SSM); this module builds the
|
|
5
|
+
* single wrapped script that runs on each box: it sets a strict shell, runs the
|
|
6
|
+
* recipe body as the requested user with a login shell (so PATH/pantry env are
|
|
7
|
+
* loaded), and prints clear begin/end markers + the exit code so captured
|
|
8
|
+
* output is easy to scan.
|
|
9
|
+
*/
|
|
10
|
+
export interface ServerRecipeOptions {
|
|
11
|
+
/** Recipe name (shown in the output markers). */
|
|
12
|
+
name: string;
|
|
13
|
+
/** The recipe body — bash command lines run on the server. */
|
|
14
|
+
script: string[];
|
|
15
|
+
/**
|
|
16
|
+
* User to run the recipe as. Defaults to `root`. A non-root user is invoked
|
|
17
|
+
* via `runuser -l` so it gets a login shell (profile + PATH, incl. pantry).
|
|
18
|
+
*/
|
|
19
|
+
user?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the wrapped recipe script run on a server. The body is written to a
|
|
23
|
+
* temp file and executed as `user`; the wrapper captures the exit code and
|
|
24
|
+
* prints `__TS_CLOUD_RECIPE_*__` markers around the run for the driver to parse.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildServerRecipeScript(options: ServerRecipeOptions): string[];
|
package/dist/index.js
CHANGED
|
@@ -25946,6 +25946,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25946
25946
|
TSCLOUD_LAMBDA_MODE: mode,
|
|
25947
25947
|
TSCLOUD_ENV: environment,
|
|
25948
25948
|
...app.octane ? { TSCLOUD_OCTANE: "1" } : {},
|
|
25949
|
+
...app.scheduler === "sub-minute" ? { TSCLOUD_SCHEDULER: "sub-minute" } : {},
|
|
25949
25950
|
...cacheEnabled ? { TSCLOUD_CACHE_TABLE: `${slug}-${environment}-cache` } : {},
|
|
25950
25951
|
...hasQueue ? { TSCLOUD_QUEUE: queueNames[0] } : {},
|
|
25951
25952
|
...app.env ?? {}
|
|
@@ -25965,7 +25966,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25965
25966
|
]
|
|
25966
25967
|
}
|
|
25967
25968
|
} : {};
|
|
25968
|
-
function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency) {
|
|
25969
|
+
function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency, tmp = tmpStorage) {
|
|
25969
25970
|
resources[`${logicalId}LogGroup`] = {
|
|
25970
25971
|
Type: "AWS::Logs::LogGroup",
|
|
25971
25972
|
Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: 14 }
|
|
@@ -25990,17 +25991,17 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25990
25991
|
Timeout: timeout,
|
|
25991
25992
|
Role: Fn2.getAtt("AppRole", "Arn"),
|
|
25992
25993
|
Environment: { Variables: baseEnv(mode) },
|
|
25993
|
-
EphemeralStorage: { Size:
|
|
25994
|
+
EphemeralStorage: { Size: tmp },
|
|
25994
25995
|
...codeProps,
|
|
25995
25996
|
...reservedConcurrency !== undefined ? { ReservedConcurrentExecutions: reservedConcurrency } : {},
|
|
25996
25997
|
...vpcConfig
|
|
25997
25998
|
}
|
|
25998
25999
|
};
|
|
25999
26000
|
}
|
|
26000
|
-
addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency);
|
|
26001
|
-
addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900);
|
|
26001
|
+
addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency, tmpStorage);
|
|
26002
|
+
addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900, undefined, app.cliTmpStorage ?? tmpStorage);
|
|
26002
26003
|
if (hasQueue)
|
|
26003
|
-
addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120);
|
|
26004
|
+
addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120, undefined, app.queueTmpStorage ?? tmpStorage);
|
|
26004
26005
|
resources.HttpApi = {
|
|
26005
26006
|
Type: "AWS::ApiGatewayV2::Api",
|
|
26006
26007
|
Properties: {
|
|
@@ -26047,6 +26048,59 @@ function composeServerlessAppTemplate(opts) {
|
|
|
26047
26048
|
Value: Fn2.getAtt("HttpApi", "ApiEndpoint")
|
|
26048
26049
|
};
|
|
26049
26050
|
outputs.HttpApiId = { Description: "HTTP API id", Value: Fn2.ref("HttpApi") };
|
|
26051
|
+
const domains = (Array.isArray(app.domain) ? app.domain : app.domain ? [app.domain] : []).filter(Boolean);
|
|
26052
|
+
if (domains.length) {
|
|
26053
|
+
if (!app.certificateArn && !app.hostedZoneId) {
|
|
26054
|
+
throw new Error("serverless app: a custom `domain` needs either `certificateArn` (pre-issued, regional) or `hostedZoneId` (to auto-issue + validate an ACM cert).");
|
|
26055
|
+
}
|
|
26056
|
+
let certRef = app.certificateArn;
|
|
26057
|
+
if (!certRef) {
|
|
26058
|
+
resources.HttpCertificate = {
|
|
26059
|
+
Type: "AWS::CertificateManager::Certificate",
|
|
26060
|
+
Properties: {
|
|
26061
|
+
DomainName: domains[0],
|
|
26062
|
+
...domains.length > 1 ? { SubjectAlternativeNames: domains.slice(1) } : {},
|
|
26063
|
+
ValidationMethod: "DNS",
|
|
26064
|
+
DomainValidationOptions: domains.map((d) => ({ DomainName: d, HostedZoneId: app.hostedZoneId }))
|
|
26065
|
+
}
|
|
26066
|
+
};
|
|
26067
|
+
certRef = Fn2.ref("HttpCertificate");
|
|
26068
|
+
}
|
|
26069
|
+
domains.forEach((d, i) => {
|
|
26070
|
+
const dn = `HttpDomain${i}`;
|
|
26071
|
+
resources[dn] = {
|
|
26072
|
+
Type: "AWS::ApiGatewayV2::DomainName",
|
|
26073
|
+
Properties: {
|
|
26074
|
+
DomainName: d,
|
|
26075
|
+
DomainNameConfigurations: [{ CertificateArn: certRef, EndpointType: "REGIONAL" }]
|
|
26076
|
+
}
|
|
26077
|
+
};
|
|
26078
|
+
resources[`HttpApiMapping${i}`] = {
|
|
26079
|
+
Type: "AWS::ApiGatewayV2::ApiMapping",
|
|
26080
|
+
DependsOn: ["HttpStage"],
|
|
26081
|
+
Properties: { ApiId: Fn2.ref("HttpApi"), DomainName: Fn2.ref(dn), Stage: "$default" }
|
|
26082
|
+
};
|
|
26083
|
+
if (app.hostedZoneId) {
|
|
26084
|
+
resources[`HttpDomainRecord${i}`] = {
|
|
26085
|
+
Type: "AWS::Route53::RecordSet",
|
|
26086
|
+
Properties: {
|
|
26087
|
+
HostedZoneId: app.hostedZoneId,
|
|
26088
|
+
Name: d,
|
|
26089
|
+
Type: "A",
|
|
26090
|
+
AliasTarget: {
|
|
26091
|
+
DNSName: Fn2.getAtt(dn, "RegionalDomainName"),
|
|
26092
|
+
HostedZoneId: Fn2.getAtt(dn, "RegionalHostedZoneId")
|
|
26093
|
+
}
|
|
26094
|
+
}
|
|
26095
|
+
};
|
|
26096
|
+
}
|
|
26097
|
+
outputs[`CustomDomain${i}`] = { Description: `Custom domain ${d}`, Value: d };
|
|
26098
|
+
outputs[`CustomDomainTarget${i}`] = {
|
|
26099
|
+
Description: `Point ${d} (CNAME/alias) at this APIGW regional domain`,
|
|
26100
|
+
Value: Fn2.getAtt(dn, "RegionalDomainName")
|
|
26101
|
+
};
|
|
26102
|
+
});
|
|
26103
|
+
}
|
|
26050
26104
|
if (hasQueue) {
|
|
26051
26105
|
resources.AppQueueDlq = {
|
|
26052
26106
|
Type: "AWS::SQS::Queue",
|
|
@@ -83487,31 +83541,63 @@ function buildDatabaseSetupScript(database, services = {}) {
|
|
|
83487
83541
|
if (usePostgres && !useMysql && !useMariadb) {
|
|
83488
83542
|
const pgIdent = (v) => `"${v.replace(/"/g, '""')}"`;
|
|
83489
83543
|
const pgLit = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
83544
|
+
const pgEnsureRole = (u, p) => [
|
|
83545
|
+
"DO $$ BEGIN",
|
|
83546
|
+
` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${pgLit(u)}) THEN`,
|
|
83547
|
+
` CREATE ROLE ${pgIdent(u)} LOGIN PASSWORD ${pgLit(p)};`,
|
|
83548
|
+
" ELSE",
|
|
83549
|
+
` ALTER ROLE ${pgIdent(u)} LOGIN PASSWORD ${pgLit(p)};`,
|
|
83550
|
+
" END IF;",
|
|
83551
|
+
"END $$;"
|
|
83552
|
+
];
|
|
83553
|
+
const pgGrant = (u) => {
|
|
83554
|
+
const dbs = u.databases && u.databases.length > 0 ? u.databases : [name];
|
|
83555
|
+
const lines = [];
|
|
83556
|
+
for (const db of dbs) {
|
|
83557
|
+
if (u.access === "readonly") {
|
|
83558
|
+
lines.push(`GRANT CONNECT ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`, `\\connect ${pgIdent(db)}`, `GRANT USAGE ON SCHEMA public TO ${pgIdent(u.username)};`, `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${pgIdent(u.username)};`, `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${pgIdent(u.username)};`, "\\connect postgres");
|
|
83559
|
+
} else {
|
|
83560
|
+
lines.push(`GRANT ALL PRIVILEGES ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`);
|
|
83561
|
+
}
|
|
83562
|
+
}
|
|
83563
|
+
return lines;
|
|
83564
|
+
};
|
|
83565
|
+
const extraUsers2 = database.users || [];
|
|
83490
83566
|
return [
|
|
83491
83567
|
pantryEnvActivation(),
|
|
83492
83568
|
"for i in $(seq 1 30); do pg_isready -h 127.0.0.1 -p 5432 -q && break; sleep 2; done",
|
|
83493
83569
|
"psql -h 127.0.0.1 -p 5432 -U postgres <<'TS_CLOUD_PG_EOF'",
|
|
83494
|
-
|
|
83495
|
-
` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${pgLit(user)}) THEN`,
|
|
83496
|
-
` CREATE ROLE ${pgIdent(user)} LOGIN PASSWORD ${pgLit(pass)};`,
|
|
83497
|
-
" END IF;",
|
|
83498
|
-
"END $$;",
|
|
83570
|
+
...pgEnsureRole(user, pass),
|
|
83499
83571
|
`SELECT 'CREATE DATABASE ${pgIdent(name)} OWNER ${pgIdent(user)}' ` + `WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${pgLit(name)})\\gexec`,
|
|
83572
|
+
...extraUsers2.flatMap((u) => [...pgEnsureRole(u.username, u.password), ...pgGrant(u)]),
|
|
83500
83573
|
"TS_CLOUD_PG_EOF"
|
|
83501
83574
|
];
|
|
83502
83575
|
}
|
|
83503
83576
|
const sock = useMariadb ? "/var/lib/pantry/mariadb/mariadbd.sock" : "/var/lib/pantry/mysql/mysqld.sock";
|
|
83504
83577
|
const ident = (v) => v.replace(/`/g, "``");
|
|
83505
83578
|
const lit = (v) => v.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
83579
|
+
const mysqlUser = (u) => {
|
|
83580
|
+
const dbs = u.databases && u.databases.length > 0 ? u.databases : [name];
|
|
83581
|
+
const priv = u.access === "readonly" ? "SELECT" : "ALL PRIVILEGES";
|
|
83582
|
+
const lines = [
|
|
83583
|
+
`CREATE USER IF NOT EXISTS '${lit(u.username)}'@'%' IDENTIFIED BY '${lit(u.password)}';`,
|
|
83584
|
+
`CREATE USER IF NOT EXISTS '${lit(u.username)}'@'localhost' IDENTIFIED BY '${lit(u.password)}';`,
|
|
83585
|
+
`ALTER USER '${lit(u.username)}'@'%' IDENTIFIED BY '${lit(u.password)}';`,
|
|
83586
|
+
`ALTER USER '${lit(u.username)}'@'localhost' IDENTIFIED BY '${lit(u.password)}';`
|
|
83587
|
+
];
|
|
83588
|
+
for (const db of dbs) {
|
|
83589
|
+
lines.push(`GRANT ${priv} ON \`${ident(db)}\`.* TO '${lit(u.username)}'@'%';`, `GRANT ${priv} ON \`${ident(db)}\`.* TO '${lit(u.username)}'@'localhost';`);
|
|
83590
|
+
}
|
|
83591
|
+
return lines;
|
|
83592
|
+
};
|
|
83593
|
+
const extraUsers = database.users || [];
|
|
83506
83594
|
return [
|
|
83507
83595
|
pantryEnvActivation(),
|
|
83508
83596
|
`for i in $(seq 1 30); do mysqladmin --socket=${sock} -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done`,
|
|
83509
83597
|
`mysql --socket=${sock} -u root <<'TS_CLOUD_SQL_EOF'`,
|
|
83510
83598
|
`CREATE DATABASE IF NOT EXISTS \`${ident(name)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,
|
|
83511
|
-
|
|
83512
|
-
|
|
83513
|
-
`GRANT ALL PRIVILEGES ON \`${ident(name)}\`.* TO '${lit(user)}'@'%';`,
|
|
83514
|
-
`GRANT ALL PRIVILEGES ON \`${ident(name)}\`.* TO '${lit(user)}'@'localhost';`,
|
|
83599
|
+
...mysqlUser({ username: user, password: pass }),
|
|
83600
|
+
...extraUsers.flatMap(mysqlUser),
|
|
83515
83601
|
"FLUSH PRIVILEGES;",
|
|
83516
83602
|
"TS_CLOUD_SQL_EOF"
|
|
83517
83603
|
];
|
|
@@ -83730,6 +83816,12 @@ function buildNginxServiceScript(projectDir = "/opt/pantry") {
|
|
|
83730
83816
|
" fastcgi_temp_path /var/lib/nginx/fastcgi;",
|
|
83731
83817
|
" uwsgi_temp_path /var/lib/nginx/uwsgi;",
|
|
83732
83818
|
" scgi_temp_path /var/lib/nginx/scgi;",
|
|
83819
|
+
" server {",
|
|
83820
|
+
" listen 80 default_server;",
|
|
83821
|
+
" listen [::]:80 default_server;",
|
|
83822
|
+
" server_name _;",
|
|
83823
|
+
" return 444;",
|
|
83824
|
+
" }",
|
|
83733
83825
|
" include /etc/nginx/sites-enabled/*;",
|
|
83734
83826
|
"}",
|
|
83735
83827
|
"TS_CLOUD_NGINXCONF_EOF",
|
|
@@ -85673,10 +85765,22 @@ function resolveSslProvider(site) {
|
|
|
85673
85765
|
return site.ssl.provider;
|
|
85674
85766
|
return site.domain ? "letsencrypt" : "none";
|
|
85675
85767
|
}
|
|
85676
|
-
|
|
85768
|
+
var DNS_PLUGINS = {
|
|
85769
|
+
cloudflare: { pkg: "python3-certbot-dns-cloudflare", plugin: "dns-cloudflare" },
|
|
85770
|
+
route53: { pkg: "python3-certbot-dns-route53", plugin: "dns-route53" },
|
|
85771
|
+
digitalocean: { pkg: "python3-certbot-dns-digitalocean", plugin: "dns-digitalocean" },
|
|
85772
|
+
google: { pkg: "python3-certbot-dns-google", plugin: "dns-google" }
|
|
85773
|
+
};
|
|
85774
|
+
function dnsCredentialsPath(provider) {
|
|
85775
|
+
return `/etc/letsencrypt/ts-cloud-${provider}.ini`;
|
|
85776
|
+
}
|
|
85777
|
+
function buildCertbotInstallScript(dns2) {
|
|
85778
|
+
const plugin = dns2 ? DNS_PLUGINS[dns2.provider] : undefined;
|
|
85779
|
+
const pkgs = ["certbot", "python3-certbot-nginx", ...plugin ? [plugin.pkg] : []].join(" ");
|
|
85780
|
+
const installLine = plugin ? `apt-get update -y && apt-get install -y ${pkgs}` : `command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y ${pkgs}; }`;
|
|
85677
85781
|
return [
|
|
85678
85782
|
"export DEBIAN_FRONTEND=noninteractive",
|
|
85679
|
-
|
|
85783
|
+
installLine,
|
|
85680
85784
|
"mkdir -p /etc/letsencrypt/renewal-hooks/deploy",
|
|
85681
85785
|
"cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'",
|
|
85682
85786
|
"#!/bin/sh",
|
|
@@ -85687,15 +85791,34 @@ function buildCertbotInstallScript() {
|
|
|
85687
85791
|
"systemctl start certbot.timer 2>/dev/null || true"
|
|
85688
85792
|
];
|
|
85689
85793
|
}
|
|
85690
|
-
function
|
|
85691
|
-
|
|
85692
|
-
|
|
85693
|
-
|
|
85694
|
-
|
|
85695
|
-
|
|
85696
|
-
|
|
85697
|
-
|
|
85794
|
+
function buildDnsCredentialsScript(dns2) {
|
|
85795
|
+
if (!dns2.credentials || Object.keys(dns2.credentials).length === 0)
|
|
85796
|
+
return [];
|
|
85797
|
+
const file = dnsCredentialsPath(dns2.provider);
|
|
85798
|
+
const lines = Object.entries(dns2.credentials).map(([k, v]) => `${k} = ${v}`).join(`
|
|
85799
|
+
`);
|
|
85800
|
+
return [
|
|
85801
|
+
`cat > ${file} <<'TS_CLOUD_DNSCREDS_EOF'`,
|
|
85802
|
+
lines,
|
|
85803
|
+
"TS_CLOUD_DNSCREDS_EOF",
|
|
85804
|
+
`chmod 600 ${file}`
|
|
85698
85805
|
];
|
|
85806
|
+
}
|
|
85807
|
+
function buildCertbotIssueScript(options) {
|
|
85808
|
+
const dns2 = options.dns;
|
|
85809
|
+
const domains = options.wildcard ? [`*.${options.domain}`, options.domain] : [options.domain, ...options.aliases || []].filter(Boolean);
|
|
85810
|
+
const args = ["certbot", "--non-interactive", "--agree-tos", "--keep-until-expiring"];
|
|
85811
|
+
if (dns2) {
|
|
85812
|
+
const plugin = DNS_PLUGINS[dns2.provider].plugin;
|
|
85813
|
+
args.push(`--${plugin}`);
|
|
85814
|
+
if (dns2.provider !== "route53" && dns2.credentials)
|
|
85815
|
+
args.push(`--${plugin}-credentials ${dnsCredentialsPath(dns2.provider)}`);
|
|
85816
|
+
if (typeof dns2.propagationSeconds === "number" && dns2.provider !== "route53")
|
|
85817
|
+
args.push(`--${plugin}-propagation-seconds ${dns2.propagationSeconds}`);
|
|
85818
|
+
args.push("certonly");
|
|
85819
|
+
} else {
|
|
85820
|
+
args.push("--nginx", options.redirect === false ? "--no-redirect" : "--redirect");
|
|
85821
|
+
}
|
|
85699
85822
|
if (options.email)
|
|
85700
85823
|
args.push(`-m ${options.email}`);
|
|
85701
85824
|
else
|
|
@@ -85707,12 +85830,20 @@ function buildCertbotIssueScript(options) {
|
|
|
85707
85830
|
function buildSslScript(site) {
|
|
85708
85831
|
if (resolveSslProvider(site) !== "letsencrypt" || !site.domain)
|
|
85709
85832
|
return [];
|
|
85833
|
+
const ssl = site.ssl;
|
|
85834
|
+
const dns2 = ssl?.dns;
|
|
85835
|
+
const wildcard = ssl?.wildcard === true;
|
|
85836
|
+
if (wildcard && !dns2)
|
|
85837
|
+
return [];
|
|
85710
85838
|
return [
|
|
85711
|
-
...buildCertbotInstallScript(),
|
|
85839
|
+
...buildCertbotInstallScript(dns2),
|
|
85840
|
+
...dns2 ? buildDnsCredentialsScript(dns2) : [],
|
|
85712
85841
|
...buildCertbotIssueScript({
|
|
85713
85842
|
domain: site.domain,
|
|
85714
85843
|
aliases: site.aliases,
|
|
85715
|
-
email:
|
|
85844
|
+
email: ssl?.email,
|
|
85845
|
+
wildcard,
|
|
85846
|
+
dns: dns2
|
|
85716
85847
|
})
|
|
85717
85848
|
];
|
|
85718
85849
|
}
|
|
@@ -85857,6 +85988,21 @@ function writeSharedEnv(sharedEnvPath, env2) {
|
|
|
85857
85988
|
`chmod 600 ${sharedEnvPath}`
|
|
85858
85989
|
];
|
|
85859
85990
|
}
|
|
85991
|
+
function writeFileHeredoc2(path, body, marker) {
|
|
85992
|
+
return [`cat > ${path} <<'${marker}'`, body.replace(/\n$/, ""), marker, `chmod 600 ${path}`];
|
|
85993
|
+
}
|
|
85994
|
+
function buildCredentialFiles(releaseDir, creds) {
|
|
85995
|
+
if (!creds)
|
|
85996
|
+
return [];
|
|
85997
|
+
const out = [];
|
|
85998
|
+
if (creds.composerAuth) {
|
|
85999
|
+
const json = typeof creds.composerAuth === "string" ? creds.composerAuth : JSON.stringify(creds.composerAuth, null, 2);
|
|
86000
|
+
out.push(...writeFileHeredoc2(`${releaseDir}/auth.json`, json, "TS_CLOUD_AUTHJSON_EOF"));
|
|
86001
|
+
}
|
|
86002
|
+
if (creds.npmrc)
|
|
86003
|
+
out.push(...writeFileHeredoc2(`${releaseDir}/.npmrc`, creds.npmrc, "TS_CLOUD_NPMRC_EOF"));
|
|
86004
|
+
return out;
|
|
86005
|
+
}
|
|
85860
86006
|
function buildLaravelDeployScript(options) {
|
|
85861
86007
|
const { siteName, site, releaseId, commit } = options;
|
|
85862
86008
|
if (!site.repository?.url)
|
|
@@ -85883,6 +86029,7 @@ function buildLaravelDeployScript(options) {
|
|
|
85883
86029
|
out.push(...buildGitCheckoutScript({ repository: site.repository, releaseDir: paths.release, commit }));
|
|
85884
86030
|
out.push(...buildLinkSharedPaths(paths, sharedPaths));
|
|
85885
86031
|
out.push(`cd ${paths.release}`);
|
|
86032
|
+
out.push(...buildCredentialFiles(paths.release, site.credentials));
|
|
85886
86033
|
out.push(`chown -R www-data:www-data ${paths.shared}/storage 2>/dev/null || true`, `[ -d ${paths.release}/bootstrap/cache ] && chown -R www-data:www-data ${paths.release}/bootstrap/cache 2>/dev/null || true`, `chmod -R ug+rwX ${paths.shared}/storage 2>/dev/null || true`);
|
|
85887
86034
|
} else if (line === MACRO_ACTIVATE_RELEASE) {
|
|
85888
86035
|
out.push(...buildActivateRelease(paths));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/ts-cloud",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"test": "bun test"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@ts-cloud/aws-types": "0.5.
|
|
93
|
-
"@ts-cloud/core": "0.5.
|
|
92
|
+
"@ts-cloud/aws-types": "0.5.1",
|
|
93
|
+
"@ts-cloud/core": "0.5.1",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|