@stacksjs/ts-cloud 0.4.2 → 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.
- package/dist/bin/cli.js +7247 -1030
- package/dist/deploy/management-dashboard.d.ts +39 -0
- package/dist/drivers/shared/certbot.d.ts +21 -7
- package/dist/drivers/shared/laravel-deploy.d.ts +6 -1
- package/dist/drivers/shared/releases.d.ts +11 -0
- package/dist/drivers/shared/server-recipes.d.ts +26 -0
- package/dist/index.js +229 -26
- package/dist/ui/404.html +22 -0
- package/dist/ui/index.html +995 -0
- package/dist/ui/serverless.html +995 -0
- package/package.json +3 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-deploy of the ts-cloud management dashboard (the `@ts-cloud/ui` stx app)
|
|
3
|
+
* on every server provision/deploy.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the UI directory (the repo's local `ui/`, else the prebuilt UI that
|
|
6
|
+
* ships inside the installed package at `dist/ui`), derives a `dashboard.<apex>`
|
|
7
|
+
* host, and injects it into `config.sites` as a server-static site. It is served
|
|
8
|
+
* behind htpasswd ONLY when `TS_CLOUD_UI_PASSWORD` is set; when it is not, the
|
|
9
|
+
* dashboard is served without auth (no password is invented).
|
|
10
|
+
*
|
|
11
|
+
* Env:
|
|
12
|
+
* - `TS_CLOUD_UI_PASSWORD` htpasswd password (unset ⇒ no auth)
|
|
13
|
+
* - `TS_CLOUD_UI_USERNAME` htpasswd user (default `admin`)
|
|
14
|
+
* - `TS_CLOUD_UI_DOMAIN` explicit dashboard host (else `dashboard.<apex>`)
|
|
15
|
+
* - `TS_CLOUD_UI_REALM` browser auth realm
|
|
16
|
+
* - `TS_CLOUD_UI_DISABLE` set truthy to skip auto-deploy
|
|
17
|
+
*/
|
|
18
|
+
import type { CloudConfig } from '@stacksjs/ts-cloud';
|
|
19
|
+
export interface EnsureDashboardLogger {
|
|
20
|
+
info: (msg: string) => void;
|
|
21
|
+
warn: (msg: string) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the UI source to ship. Prefers the repo's local `ui/` (built on the
|
|
25
|
+
* deploy machine), then the prebuilt UI bundled in the installed package.
|
|
26
|
+
* Returns `{ uiRoot, build }` or null when no UI is available.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveUiSource(cwd: string): {
|
|
29
|
+
uiRoot: string;
|
|
30
|
+
build: string | false;
|
|
31
|
+
} | null;
|
|
32
|
+
/**
|
|
33
|
+
* Inject the management dashboard into `config.sites` for a server deploy.
|
|
34
|
+
* Mutates and returns `config`. Idempotent and safe to call on every deploy.
|
|
35
|
+
*/
|
|
36
|
+
export declare function ensureManagementDashboard(config: CloudConfig, options?: {
|
|
37
|
+
cwd?: string;
|
|
38
|
+
logger?: EnsureDashboardLogger;
|
|
39
|
+
}): CloudConfig;
|
|
@@ -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
|
@@ -7654,6 +7654,56 @@ function createDashboardSite(options) {
|
|
|
7654
7654
|
}
|
|
7655
7655
|
};
|
|
7656
7656
|
}
|
|
7657
|
+
function apexOf(domain) {
|
|
7658
|
+
const parts = domain.split(".").filter(Boolean);
|
|
7659
|
+
return parts.length <= 2 ? domain : parts.slice(-2).join(".");
|
|
7660
|
+
}
|
|
7661
|
+
function resolveDashboardDomain(config6, environment, explicit) {
|
|
7662
|
+
if (explicit)
|
|
7663
|
+
return explicit;
|
|
7664
|
+
const candidates = [];
|
|
7665
|
+
for (const site of Object.values(config6.sites ?? {})) {
|
|
7666
|
+
const d = site?.domain;
|
|
7667
|
+
if (typeof d === "string")
|
|
7668
|
+
candidates.push(d);
|
|
7669
|
+
else if (Array.isArray(d))
|
|
7670
|
+
candidates.push(...d.filter((x) => typeof x === "string"));
|
|
7671
|
+
}
|
|
7672
|
+
if (environment && config6.environments?.[environment]?.domain)
|
|
7673
|
+
candidates.push(config6.environments[environment].domain);
|
|
7674
|
+
const dnsDomain = config6.infrastructure?.dns?.domain;
|
|
7675
|
+
if (dnsDomain)
|
|
7676
|
+
candidates.push(dnsDomain);
|
|
7677
|
+
const base = candidates.find((d) => d && !d.startsWith("dashboard."));
|
|
7678
|
+
if (!base)
|
|
7679
|
+
return null;
|
|
7680
|
+
return `dashboard.${apexOf(base)}`;
|
|
7681
|
+
}
|
|
7682
|
+
function hasManagementDashboardSite(config6) {
|
|
7683
|
+
return Object.entries(config6.sites ?? {}).some(([name, site]) => {
|
|
7684
|
+
if (!site)
|
|
7685
|
+
return false;
|
|
7686
|
+
const root = site.root ?? "";
|
|
7687
|
+
return name === "dashboard" || root === "ui/dist" || root.endsWith("/ui/dist") || root.endsWith("/dist/ui");
|
|
7688
|
+
});
|
|
7689
|
+
}
|
|
7690
|
+
function resolveManagementDashboardSite(config6, environment, opts) {
|
|
7691
|
+
if (hasManagementDashboardSite(config6))
|
|
7692
|
+
return null;
|
|
7693
|
+
const domain = resolveDashboardDomain(config6, environment, opts.domain);
|
|
7694
|
+
if (!domain)
|
|
7695
|
+
return null;
|
|
7696
|
+
const site = {
|
|
7697
|
+
root: opts.uiRoot,
|
|
7698
|
+
deploy: "server",
|
|
7699
|
+
type: "static",
|
|
7700
|
+
domain,
|
|
7701
|
+
ssl: { provider: "letsencrypt" },
|
|
7702
|
+
...opts.build === false || opts.build === undefined ? {} : { build: opts.build },
|
|
7703
|
+
...opts.password ? { auth: { username: opts.username || "admin", password: opts.password, realm: opts.realm } } : {}
|
|
7704
|
+
};
|
|
7705
|
+
return { name: "dashboard", site };
|
|
7706
|
+
}
|
|
7657
7707
|
function deepMerge5(target, source) {
|
|
7658
7708
|
const result = { ...target };
|
|
7659
7709
|
for (const key in source) {
|
|
@@ -25896,6 +25946,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25896
25946
|
TSCLOUD_LAMBDA_MODE: mode,
|
|
25897
25947
|
TSCLOUD_ENV: environment,
|
|
25898
25948
|
...app.octane ? { TSCLOUD_OCTANE: "1" } : {},
|
|
25949
|
+
...app.scheduler === "sub-minute" ? { TSCLOUD_SCHEDULER: "sub-minute" } : {},
|
|
25899
25950
|
...cacheEnabled ? { TSCLOUD_CACHE_TABLE: `${slug}-${environment}-cache` } : {},
|
|
25900
25951
|
...hasQueue ? { TSCLOUD_QUEUE: queueNames[0] } : {},
|
|
25901
25952
|
...app.env ?? {}
|
|
@@ -25915,7 +25966,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25915
25966
|
]
|
|
25916
25967
|
}
|
|
25917
25968
|
} : {};
|
|
25918
|
-
function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency) {
|
|
25969
|
+
function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency, tmp = tmpStorage) {
|
|
25919
25970
|
resources[`${logicalId}LogGroup`] = {
|
|
25920
25971
|
Type: "AWS::Logs::LogGroup",
|
|
25921
25972
|
Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: 14 }
|
|
@@ -25940,17 +25991,17 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25940
25991
|
Timeout: timeout,
|
|
25941
25992
|
Role: Fn2.getAtt("AppRole", "Arn"),
|
|
25942
25993
|
Environment: { Variables: baseEnv(mode) },
|
|
25943
|
-
EphemeralStorage: { Size:
|
|
25994
|
+
EphemeralStorage: { Size: tmp },
|
|
25944
25995
|
...codeProps,
|
|
25945
25996
|
...reservedConcurrency !== undefined ? { ReservedConcurrentExecutions: reservedConcurrency } : {},
|
|
25946
25997
|
...vpcConfig
|
|
25947
25998
|
}
|
|
25948
25999
|
};
|
|
25949
26000
|
}
|
|
25950
|
-
addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency);
|
|
25951
|
-
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);
|
|
25952
26003
|
if (hasQueue)
|
|
25953
|
-
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);
|
|
25954
26005
|
resources.HttpApi = {
|
|
25955
26006
|
Type: "AWS::ApiGatewayV2::Api",
|
|
25956
26007
|
Properties: {
|
|
@@ -25997,6 +26048,59 @@ function composeServerlessAppTemplate(opts) {
|
|
|
25997
26048
|
Value: Fn2.getAtt("HttpApi", "ApiEndpoint")
|
|
25998
26049
|
};
|
|
25999
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
|
+
}
|
|
26000
26104
|
if (hasQueue) {
|
|
26001
26105
|
resources.AppQueueDlq = {
|
|
26002
26106
|
Type: "AWS::SQS::Queue",
|
|
@@ -83437,28 +83541,63 @@ function buildDatabaseSetupScript(database, services = {}) {
|
|
|
83437
83541
|
if (usePostgres && !useMysql && !useMariadb) {
|
|
83438
83542
|
const pgIdent = (v) => `"${v.replace(/"/g, '""')}"`;
|
|
83439
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 || [];
|
|
83440
83566
|
return [
|
|
83441
83567
|
pantryEnvActivation(),
|
|
83442
83568
|
"for i in $(seq 1 30); do pg_isready -h 127.0.0.1 -p 5432 -q && break; sleep 2; done",
|
|
83443
83569
|
"psql -h 127.0.0.1 -p 5432 -U postgres <<'TS_CLOUD_PG_EOF'",
|
|
83444
|
-
|
|
83445
|
-
` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${pgLit(user)}) THEN`,
|
|
83446
|
-
` CREATE ROLE ${pgIdent(user)} LOGIN PASSWORD ${pgLit(pass)};`,
|
|
83447
|
-
" END IF;",
|
|
83448
|
-
"END $$;",
|
|
83570
|
+
...pgEnsureRole(user, pass),
|
|
83449
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)]),
|
|
83450
83573
|
"TS_CLOUD_PG_EOF"
|
|
83451
83574
|
];
|
|
83452
83575
|
}
|
|
83576
|
+
const sock = useMariadb ? "/var/lib/pantry/mariadb/mariadbd.sock" : "/var/lib/pantry/mysql/mysqld.sock";
|
|
83453
83577
|
const ident = (v) => v.replace(/`/g, "``");
|
|
83454
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 || [];
|
|
83455
83594
|
return [
|
|
83456
83595
|
pantryEnvActivation(),
|
|
83457
|
-
|
|
83458
|
-
|
|
83596
|
+
`for i in $(seq 1 30); do mysqladmin --socket=${sock} -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done`,
|
|
83597
|
+
`mysql --socket=${sock} -u root <<'TS_CLOUD_SQL_EOF'`,
|
|
83459
83598
|
`CREATE DATABASE IF NOT EXISTS \`${ident(name)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,
|
|
83460
|
-
|
|
83461
|
-
|
|
83599
|
+
...mysqlUser({ username: user, password: pass }),
|
|
83600
|
+
...extraUsers.flatMap(mysqlUser),
|
|
83462
83601
|
"FLUSH PRIVILEGES;",
|
|
83463
83602
|
"TS_CLOUD_SQL_EOF"
|
|
83464
83603
|
];
|
|
@@ -83677,6 +83816,12 @@ function buildNginxServiceScript(projectDir = "/opt/pantry") {
|
|
|
83677
83816
|
" fastcgi_temp_path /var/lib/nginx/fastcgi;",
|
|
83678
83817
|
" uwsgi_temp_path /var/lib/nginx/uwsgi;",
|
|
83679
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
|
+
" }",
|
|
83680
83825
|
" include /etc/nginx/sites-enabled/*;",
|
|
83681
83826
|
"}",
|
|
83682
83827
|
"TS_CLOUD_NGINXCONF_EOF",
|
|
@@ -85620,10 +85765,22 @@ function resolveSslProvider(site) {
|
|
|
85620
85765
|
return site.ssl.provider;
|
|
85621
85766
|
return site.domain ? "letsencrypt" : "none";
|
|
85622
85767
|
}
|
|
85623
|
-
|
|
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}; }`;
|
|
85624
85781
|
return [
|
|
85625
85782
|
"export DEBIAN_FRONTEND=noninteractive",
|
|
85626
|
-
|
|
85783
|
+
installLine,
|
|
85627
85784
|
"mkdir -p /etc/letsencrypt/renewal-hooks/deploy",
|
|
85628
85785
|
"cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'",
|
|
85629
85786
|
"#!/bin/sh",
|
|
@@ -85634,15 +85791,34 @@ function buildCertbotInstallScript() {
|
|
|
85634
85791
|
"systemctl start certbot.timer 2>/dev/null || true"
|
|
85635
85792
|
];
|
|
85636
85793
|
}
|
|
85637
|
-
function
|
|
85638
|
-
|
|
85639
|
-
|
|
85640
|
-
|
|
85641
|
-
|
|
85642
|
-
|
|
85643
|
-
|
|
85644
|
-
|
|
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}`
|
|
85645
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
|
+
}
|
|
85646
85822
|
if (options.email)
|
|
85647
85823
|
args.push(`-m ${options.email}`);
|
|
85648
85824
|
else
|
|
@@ -85654,12 +85830,20 @@ function buildCertbotIssueScript(options) {
|
|
|
85654
85830
|
function buildSslScript(site) {
|
|
85655
85831
|
if (resolveSslProvider(site) !== "letsencrypt" || !site.domain)
|
|
85656
85832
|
return [];
|
|
85833
|
+
const ssl = site.ssl;
|
|
85834
|
+
const dns2 = ssl?.dns;
|
|
85835
|
+
const wildcard = ssl?.wildcard === true;
|
|
85836
|
+
if (wildcard && !dns2)
|
|
85837
|
+
return [];
|
|
85657
85838
|
return [
|
|
85658
|
-
...buildCertbotInstallScript(),
|
|
85839
|
+
...buildCertbotInstallScript(dns2),
|
|
85840
|
+
...dns2 ? buildDnsCredentialsScript(dns2) : [],
|
|
85659
85841
|
...buildCertbotIssueScript({
|
|
85660
85842
|
domain: site.domain,
|
|
85661
85843
|
aliases: site.aliases,
|
|
85662
|
-
email:
|
|
85844
|
+
email: ssl?.email,
|
|
85845
|
+
wildcard,
|
|
85846
|
+
dns: dns2
|
|
85663
85847
|
})
|
|
85664
85848
|
];
|
|
85665
85849
|
}
|
|
@@ -85804,6 +85988,21 @@ function writeSharedEnv(sharedEnvPath, env2) {
|
|
|
85804
85988
|
`chmod 600 ${sharedEnvPath}`
|
|
85805
85989
|
];
|
|
85806
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
|
+
}
|
|
85807
86006
|
function buildLaravelDeployScript(options) {
|
|
85808
86007
|
const { siteName, site, releaseId, commit } = options;
|
|
85809
86008
|
if (!site.repository?.url)
|
|
@@ -85830,6 +86029,7 @@ function buildLaravelDeployScript(options) {
|
|
|
85830
86029
|
out.push(...buildGitCheckoutScript({ repository: site.repository, releaseDir: paths.release, commit }));
|
|
85831
86030
|
out.push(...buildLinkSharedPaths(paths, sharedPaths));
|
|
85832
86031
|
out.push(`cd ${paths.release}`);
|
|
86032
|
+
out.push(...buildCredentialFiles(paths.release, site.credentials));
|
|
85833
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`);
|
|
85834
86034
|
} else if (line === MACRO_ACTIVATE_RELEASE) {
|
|
85835
86035
|
out.push(...buildActivateRelease(paths));
|
|
@@ -86275,9 +86475,11 @@ export {
|
|
|
86275
86475
|
resolveQueueNames,
|
|
86276
86476
|
resolveProjectStackName,
|
|
86277
86477
|
resolveObjectStorage,
|
|
86478
|
+
resolveManagementDashboardSite,
|
|
86278
86479
|
resolveHetznerApiToken,
|
|
86279
86480
|
resolveExecStart,
|
|
86280
86481
|
resolveDeployBucketName,
|
|
86482
|
+
resolveDashboardDomain,
|
|
86281
86483
|
resolveCredentials,
|
|
86282
86484
|
resolveCloudProvider,
|
|
86283
86485
|
resolveApp,
|
|
@@ -86340,6 +86542,7 @@ export {
|
|
|
86340
86542
|
hashFile,
|
|
86341
86543
|
hashDirectory,
|
|
86342
86544
|
hashBuffer,
|
|
86545
|
+
hasManagementDashboardSite,
|
|
86343
86546
|
guardDutyManager,
|
|
86344
86547
|
globalResourceManager,
|
|
86345
86548
|
getTimestamp,
|
package/dist/ui/404.html
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Page Not Found</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
9
|
+
.container { text-align: center; }
|
|
10
|
+
h1 { font-size: 6rem; margin: 0; color: #333; }
|
|
11
|
+
p { color: #666; }
|
|
12
|
+
a { color: #0066cc; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="container">
|
|
17
|
+
<h1>404</h1>
|
|
18
|
+
<p>Page not found</p>
|
|
19
|
+
<a href="/">Go home</a>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|