@kirschbaum-development/sst-laravel 0.0.3 → 0.0.5
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/AGENTS.md +28 -0
- package/README.md +101 -69
- package/bin/cli.ts +271 -0
- package/images/deploy.png +0 -0
- package/images/diagram.png +0 -0
- package/laravel-sst.ts +32 -21
- package/package.json +13 -2
- package/sst-env.d.ts +1 -1
- package/templates/sst.config.ts.template +35 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Agent Guidelines for SST Laravel
|
|
2
|
+
|
|
3
|
+
This project is an NPM package, and it is an extension of SST to add some functionality on top of it, to help deploy Laravel applications to AWS Fargate using Docker containers.
|
|
4
|
+
|
|
5
|
+
## Build/Test Commands
|
|
6
|
+
- **Publish**: `npm run publish` (publishes package to npm with public access)
|
|
7
|
+
- No test suite or linting configured in this project
|
|
8
|
+
- No build step required (TypeScript consumed directly by SST)
|
|
9
|
+
|
|
10
|
+
## Code Style & Conventions
|
|
11
|
+
- **Formatting**: 2-space indentation, LF line endings, UTF-8 charset (see `.editorconfig`)
|
|
12
|
+
- **Language**: TypeScript with SST/Pulumi types
|
|
13
|
+
- **Imports**: Use relative paths for local modules (e.g., `./src/laravel-env.js`), absolute for SST platform (e.g., `../../../.sst/platform/...`)
|
|
14
|
+
- **Types**: Use Input<T> for component props, Output<T> for Pulumi async values, explicit interfaces for public APIs
|
|
15
|
+
- **Naming**: PascalCase for classes/interfaces/types/enums, camelCase for variables/functions, kebab-case for files
|
|
16
|
+
|
|
17
|
+
## Architecture Patterns
|
|
18
|
+
- Component extends SST's `Component` base class
|
|
19
|
+
- Use `all()` and `.apply()` for Pulumi Output transformations
|
|
20
|
+
- File system operations use Node.js `fs` and `path` modules synchronously
|
|
21
|
+
- Configuration defaults: PHP 8.4, opcache enabled, auto-inject env vars
|
|
22
|
+
- Build artifacts go to `.sst/laravel` directory (managed via `pluginBuildPath`)
|
|
23
|
+
|
|
24
|
+
## Error Handling & Security
|
|
25
|
+
- Validate paths with `path.resolve()` before file operations
|
|
26
|
+
- Use `fs.existsSync()` checks before reading files
|
|
27
|
+
- Never log or expose secrets/passwords
|
|
28
|
+
- Set proper file permissions (0o755 for scripts, 0o777 for s6 executables)
|
package/README.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
# SST Laravel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SST Laravel is an unofficial extension of [SST](https://sst.dev) created by [Kirschbaum Development](https://kirschbaumdevelopment.com) to deploy your Laravel application to AWS behind a robust, reliable and scalable infrastructure, with all the power of SST.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
SST is a framework that makes it easy to build modern full-stack applications on your own infrastructure.
|
|
6
6
|
|
|
7
|
-
## What
|
|
7
|
+
## What gets deployed
|
|
8
8
|
|
|
9
|
-
Behind the scenes, this extension uses the SST Cluster + Service component, which
|
|
9
|
+
Behind the scenes, this extension uses the SST Cluster + Service component, which deploys custom Docker containers to AWS Fargate. It all gets deployed on your own AWS account, and you have full control over the infrastructure and which services are connected to your application.
|
|
10
10
|
|
|
11
11
|
This package deploys a full-blown infrastructure in AWS, with zero downtime deployments, as it can be seeing in the image below.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Behind the scenes, we use the powerful PHP containers from [Serverside Up](https://serversideup.net/open-source/docker-php/).
|
|
14
|
+
|
|
15
|
+

|
|
14
16
|
|
|
15
17
|
## Pre-requisites
|
|
16
18
|
|
|
17
19
|
1. NodeJS.
|
|
18
20
|
1. Have [SST](https://sst.dev) installed and configured.
|
|
21
|
+
1. Have [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed and configured.
|
|
22
|
+
* Guide on how to set up IAM Credentials [here](https://sst.dev/docs/iam-credentials/).
|
|
19
23
|
|
|
20
24
|
## Installation instructions
|
|
21
25
|
|
|
@@ -25,6 +29,14 @@ Pull in the package using npm:
|
|
|
25
29
|
npm install @kirschbaum/sst-laravel --save
|
|
26
30
|
```
|
|
27
31
|
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
To get started quickly, you can use the `init` command:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx sst-laravel init
|
|
38
|
+
```
|
|
39
|
+
|
|
28
40
|
## Usage
|
|
29
41
|
|
|
30
42
|
To start using, you only need to import the component in your `sst.config.ts` file:
|
|
@@ -44,13 +56,13 @@ Setting up your app to receive HTTP requests, on the `laravel-sst-demo.kdg.dev`
|
|
|
44
56
|
|
|
45
57
|
```js
|
|
46
58
|
const app = new Laravel('MyLaravelApp', {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
web: {
|
|
60
|
+
domain: 'laravel-sst-demo.kdg.dev',
|
|
61
|
+
scaling: {
|
|
62
|
+
min: 1,
|
|
63
|
+
max: 3,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
54
66
|
});
|
|
55
67
|
```
|
|
56
68
|
|
|
@@ -65,12 +77,12 @@ SST Laravel will automatically deploy and configure worker containers running yo
|
|
|
65
77
|
|
|
66
78
|
```js
|
|
67
79
|
const app = new Laravel('MyLaravelApp', {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
workers: [
|
|
81
|
+
{
|
|
82
|
+
name: 'scheduler',
|
|
83
|
+
scheduler: true,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
74
86
|
});
|
|
75
87
|
```
|
|
76
88
|
|
|
@@ -78,12 +90,12 @@ const app = new Laravel('MyLaravelApp', {
|
|
|
78
90
|
|
|
79
91
|
```js
|
|
80
92
|
const app = new Laravel('MyLaravelApp', {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
workers: [
|
|
94
|
+
{
|
|
95
|
+
name: 'horizon',
|
|
96
|
+
horizon: true,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
87
99
|
});
|
|
88
100
|
```
|
|
89
101
|
|
|
@@ -91,22 +103,22 @@ const app = new Laravel('MyLaravelApp', {
|
|
|
91
103
|
|
|
92
104
|
```js
|
|
93
105
|
const app = new Laravel('MyLaravelApp', {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
},
|
|
101
|
-
'queue': {
|
|
102
|
-
command: 'php artisan queue:work',
|
|
103
|
-
},
|
|
104
|
-
'pulse': {
|
|
105
|
-
command: 'php artisan pulse:work',
|
|
106
|
-
},
|
|
107
|
-
},
|
|
106
|
+
workers: [
|
|
107
|
+
{
|
|
108
|
+
name: 'worker',
|
|
109
|
+
tasks: {
|
|
110
|
+
'scheduler': {
|
|
111
|
+
command: 'php artisan schedule:work',
|
|
108
112
|
},
|
|
109
|
-
|
|
113
|
+
'queue': {
|
|
114
|
+
command: 'php artisan queue:work',
|
|
115
|
+
},
|
|
116
|
+
'pulse': {
|
|
117
|
+
command: 'php artisan pulse:work',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
110
122
|
});
|
|
111
123
|
```
|
|
112
124
|
|
|
@@ -152,7 +164,7 @@ const redis = new sst.aws.Redis("MyRedis", { vpc });
|
|
|
152
164
|
const bucket = new sst.aws.Bucket("MyBucket");
|
|
153
165
|
|
|
154
166
|
const app = new Laravel('MyLaravelApp', {
|
|
155
|
-
|
|
167
|
+
link: [database, redis, bucket],
|
|
156
168
|
});
|
|
157
169
|
```
|
|
158
170
|
|
|
@@ -164,27 +176,27 @@ If you need to customize the environment variable names for your resources, you
|
|
|
164
176
|
|
|
165
177
|
```js
|
|
166
178
|
const app = new Laravel('MyLaravelApp', {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
link: [
|
|
180
|
+
email,
|
|
181
|
+
{
|
|
182
|
+
resource: database,
|
|
183
|
+
environment: (database: sst.aws.Postgres) => ({
|
|
184
|
+
CUSTOM_DB_HOST: database.host.apply(host => host.toString()),
|
|
185
|
+
CUSTOM_DB_NAME: database.database.apply(database => database.toString()),
|
|
186
|
+
CUSTOM_DB_USER: database.username.apply(username => username.toString()),
|
|
187
|
+
CUSTOM_DB_PASSWORD: database.password.apply(password => password.toString()),
|
|
188
|
+
})
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
resource: redis,
|
|
192
|
+
environment: (redis: sst.aws.Redis) => ({
|
|
193
|
+
QUEUE_CONNECTION: 'redis',
|
|
194
|
+
QUEUE_REDIS_HOST: redis.host.apply(host => host ? `tls://${host}` : ''),
|
|
195
|
+
QUEUE_REDIS_PORT: redis.port.apply(port => port.toString()),
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
web: {}
|
|
188
200
|
});
|
|
189
201
|
```
|
|
190
202
|
|
|
@@ -212,13 +224,13 @@ You can configure the PHP version, custom environment variables and a custom dep
|
|
|
212
224
|
|
|
213
225
|
```js
|
|
214
226
|
const app = new Laravel('MyLaravelApp', {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
},
|
|
227
|
+
config: {
|
|
228
|
+
php: 8.4,
|
|
229
|
+
opcache: true,
|
|
230
|
+
deployment: {
|
|
231
|
+
script: './infra/deploy.sh'
|
|
221
232
|
},
|
|
233
|
+
},
|
|
222
234
|
});
|
|
223
235
|
```
|
|
224
236
|
|
|
@@ -241,9 +253,29 @@ echo "🚀 Running Laravel Migrations..."
|
|
|
241
253
|
php artisan migrate --force
|
|
242
254
|
```
|
|
243
255
|
|
|
244
|
-
##
|
|
256
|
+
## Accessing Containers
|
|
257
|
+
|
|
258
|
+
Using the `sst-laravel` CLI tool, you can easily connect to your running ECS containers for debugging and troubleshooting.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npx sst-laravel ssh --stage production
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
This will list all running tasks in your cluster and let you choose which one to connect to.
|
|
265
|
+
|
|
266
|
+
**Connect to a specific service:**
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npx sst-laravel ssh web --stage production
|
|
270
|
+
npx sst-laravel ssh worker --stage production
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
If you are naming your workers differently, you can specify the worker name:
|
|
245
274
|
|
|
246
|
-
|
|
275
|
+
```bash
|
|
276
|
+
npx sst-laravel ssh {worker-name} --stage production
|
|
277
|
+
npx sst-laravel ssh worker --stage production
|
|
278
|
+
```
|
|
247
279
|
|
|
248
280
|
***
|
|
249
281
|
|
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { ECSClient, ListTasksCommand, DescribeTasksCommand, ListClustersCommand, Task } from '@aws-sdk/client-ecs';
|
|
5
|
+
import { select } from '@inquirer/prompts';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
interface SshOptions {
|
|
11
|
+
stage?: string;
|
|
12
|
+
cluster?: string;
|
|
13
|
+
region: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findSstConfig(): string | null {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const possiblePaths = [
|
|
19
|
+
path.join(cwd, 'sst.config.ts'),
|
|
20
|
+
path.join(cwd, 'sst.config.js'),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const configPath of possiblePaths) {
|
|
24
|
+
if (fs.existsSync(configPath)) {
|
|
25
|
+
return configPath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractLaravelComponents(configPath: string): string[] {
|
|
33
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
+
const regex = /new\s+Laravel\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
35
|
+
const components: string[] = [];
|
|
36
|
+
let match: RegExpExecArray | null;
|
|
37
|
+
|
|
38
|
+
while ((match = regex.exec(content)) !== null) {
|
|
39
|
+
components.push(match[1]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return components;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const program = new Command();
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.name('sst-laravel')
|
|
49
|
+
.description('CLI tools for SST Laravel deployments')
|
|
50
|
+
.version('0.0.4');
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command('init')
|
|
54
|
+
.description('Initialize a new sst.config.ts file with Laravel boilerplate')
|
|
55
|
+
.action(() => {
|
|
56
|
+
try {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const targetPath = path.join(cwd, 'sst.config.ts');
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(targetPath)) {
|
|
61
|
+
console.error('Warning: sst.config.ts already exists in the current directory.');
|
|
62
|
+
console.error('Will not overwrite existing file.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'sst.config.ts.template');
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(templatePath)) {
|
|
69
|
+
console.error('Error: Template file not found.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
74
|
+
|
|
75
|
+
const envPath = path.join(cwd, '.env');
|
|
76
|
+
let appName = 'my-laravel-app';
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(envPath)) {
|
|
79
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
80
|
+
const appNameMatch = envContent.match(/^APP_NAME=(.+)$/m);
|
|
81
|
+
|
|
82
|
+
if (appNameMatch && appNameMatch[1]) {
|
|
83
|
+
const rawAppName = appNameMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
84
|
+
appName = rawAppName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
85
|
+
console.log(`Using APP_NAME from .env: ${rawAppName}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
templateContent = templateContent.replace('my-laravel-app', appName);
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(targetPath, templateContent, 'utf-8');
|
|
92
|
+
|
|
93
|
+
console.log('Successfully created sst.config.ts');
|
|
94
|
+
console.log('You can now customize the configuration for your Laravel application.');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Error:', (error as Error).message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command('ssh')
|
|
103
|
+
.description('SSH into a running ECS task')
|
|
104
|
+
.argument('[service]', 'Service to connect to (web, worker, or worker name) - optional')
|
|
105
|
+
.option('-s, --stage <stage>', 'SST stage name (required)')
|
|
106
|
+
.option('-c, --cluster <cluster>', 'ECS cluster name (optional, auto-detected from SST config)')
|
|
107
|
+
.option('-r, --region <region>', 'AWS region', process.env.AWS_REGION || 'us-east-1')
|
|
108
|
+
.action(async (service: string | undefined, options: SshOptions) => {
|
|
109
|
+
try {
|
|
110
|
+
const region = options.region;
|
|
111
|
+
const stage = options.stage;
|
|
112
|
+
|
|
113
|
+
if (!stage) {
|
|
114
|
+
console.error('Error: Stage is required. Use --stage flag to specify the SST stage.');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ecsClient = new ECSClient({ region });
|
|
119
|
+
|
|
120
|
+
let clusterArn = options.cluster;
|
|
121
|
+
|
|
122
|
+
if (!clusterArn) {
|
|
123
|
+
const configPath = findSstConfig();
|
|
124
|
+
if (!configPath) {
|
|
125
|
+
console.error('Error: Could not find sst.config.ts or sst.config.js in current directory.');
|
|
126
|
+
console.error('Please use --cluster flag to specify cluster ARN manually.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const components = extractLaravelComponents(configPath);
|
|
131
|
+
|
|
132
|
+
if (components.length === 0) {
|
|
133
|
+
console.error('Error: No Laravel components found in SST config.');
|
|
134
|
+
console.error('Please use --cluster flag to specify cluster ARN manually.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (components.length > 1) {
|
|
139
|
+
console.error('Error: Multiple Laravel components found in SST config.');
|
|
140
|
+
console.error(`Found: ${components.join(', ')}`);
|
|
141
|
+
console.error('Please use --cluster flag to specify which cluster to connect to.');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const componentName = components[0].replace(/-/g, '');
|
|
146
|
+
const clusterPattern = `${stage}-${componentName}Cluster`;
|
|
147
|
+
|
|
148
|
+
console.log(`Looking for cluster matching pattern: *${clusterPattern}`);
|
|
149
|
+
|
|
150
|
+
const listClustersCommand = new ListClustersCommand({});
|
|
151
|
+
const listClustersResponse = await ecsClient.send(listClustersCommand);
|
|
152
|
+
|
|
153
|
+
if (!listClustersResponse.clusterArns || listClustersResponse.clusterArns.length === 0) {
|
|
154
|
+
console.error('Error: No ECS clusters found in this region.');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const matchingCluster = listClustersResponse.clusterArns.find(arn => {
|
|
159
|
+
const clusterName = arn.split('/').pop();
|
|
160
|
+
return clusterName?.includes(stage) && clusterName?.includes(componentName);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!matchingCluster) {
|
|
164
|
+
console.error(`Error: No cluster found matching stage "${stage}" and component "${components[0]}".`);
|
|
165
|
+
console.error('Available clusters:');
|
|
166
|
+
listClustersResponse.clusterArns.forEach(arn => {
|
|
167
|
+
console.error(` - ${arn.split('/').pop()}`);
|
|
168
|
+
});
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
clusterArn = matchingCluster;
|
|
173
|
+
console.log(`Auto-detected cluster: ${clusterArn.split('/').pop()}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`Cluster ARN: ${clusterArn}`);
|
|
177
|
+
|
|
178
|
+
const listTasksCommand = new ListTasksCommand({
|
|
179
|
+
cluster: clusterArn,
|
|
180
|
+
desiredStatus: 'RUNNING'
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const listTasksResponse = await ecsClient.send(listTasksCommand);
|
|
184
|
+
|
|
185
|
+
if (!listTasksResponse.taskArns || listTasksResponse.taskArns.length === 0) {
|
|
186
|
+
console.error('No running tasks found in cluster');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const describeTasksCommand = new DescribeTasksCommand({
|
|
191
|
+
cluster: clusterArn,
|
|
192
|
+
tasks: listTasksResponse.taskArns
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const describeTasksResponse = await ecsClient.send(describeTasksCommand);
|
|
196
|
+
|
|
197
|
+
let matchingTask: Task | undefined;
|
|
198
|
+
|
|
199
|
+
if (service) {
|
|
200
|
+
let servicePrefix: string;
|
|
201
|
+
if (service === 'web') {
|
|
202
|
+
servicePrefix = '-web';
|
|
203
|
+
} else if (service === 'worker') {
|
|
204
|
+
servicePrefix = '-worker';
|
|
205
|
+
} else {
|
|
206
|
+
servicePrefix = `-${service}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
matchingTask = describeTasksResponse.tasks?.find(task => {
|
|
210
|
+
const containerName = task.containers?.[0]?.name || '';
|
|
211
|
+
return containerName.toLowerCase().includes(servicePrefix.toLowerCase());
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!matchingTask) {
|
|
216
|
+
if (service) {
|
|
217
|
+
console.log(`\nNo running task found matching service: ${service}`);
|
|
218
|
+
}
|
|
219
|
+
console.log('Available tasks in cluster:\n');
|
|
220
|
+
|
|
221
|
+
const choices = describeTasksResponse.tasks?.map(task => {
|
|
222
|
+
const taskId = task.taskArn?.split('/').pop() || '';
|
|
223
|
+
const containerName = task.containers?.[0]?.name || 'unknown';
|
|
224
|
+
const status = task.lastStatus || 'unknown';
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
name: `${containerName} (${taskId.substring(0, 8)}...) - ${status}`,
|
|
228
|
+
value: task,
|
|
229
|
+
description: `Task: ${taskId}`
|
|
230
|
+
};
|
|
231
|
+
}) || [];
|
|
232
|
+
|
|
233
|
+
if (choices.length === 0) {
|
|
234
|
+
console.error('No tasks available to select from.');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
matchingTask = await select({
|
|
239
|
+
message: 'Select a task to connect to:',
|
|
240
|
+
choices
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const taskId = matchingTask.taskArn?.split('/').pop();
|
|
245
|
+
|
|
246
|
+
console.log(`Connecting to task: ${taskId}`);
|
|
247
|
+
|
|
248
|
+
const awsCommand = spawn('aws', [
|
|
249
|
+
'ecs',
|
|
250
|
+
'execute-command',
|
|
251
|
+
'--cluster', clusterArn,
|
|
252
|
+
'--task', taskId!,
|
|
253
|
+
'--container', matchingTask.containers?.[0]?.name || '',
|
|
254
|
+
'--interactive',
|
|
255
|
+
'--command', '/bin/bash'
|
|
256
|
+
], {
|
|
257
|
+
stdio: 'inherit',
|
|
258
|
+
env: { ...process.env, AWS_REGION: region }
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
awsCommand.on('exit', (code) => {
|
|
262
|
+
process.exit(code || 0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('Error:', (error as Error).message);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
program.parse();
|
|
Binary file
|
package/images/diagram.png
CHANGED
|
Binary file
|
package/laravel-sst.ts
CHANGED
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import { Component } from "../../../.sst/platform/src/components/component.js";
|
|
6
|
-
import { FunctionArgs } from "../../../.sst/platform/src/components/aws/function.js"
|
|
6
|
+
import { FunctionArgs } from "../../../.sst/platform/src/components/aws/function.js";;
|
|
7
7
|
import { ComponentResourceOptions, Output, all, output } from "../../../.sst/platform/node_modules/@pulumi/pulumi/index.js";
|
|
8
8
|
import { Input } from "../../../.sst/platform/src/components/input.js";
|
|
9
|
-
import { Link } from "../../../.sst/platform/src/components/link.js";
|
|
10
9
|
import { ClusterArgs } from "../../../.sst/platform/src/components/aws/cluster.js";
|
|
11
10
|
import { ServiceArgs } from "../../../.sst/platform/src/components/aws/service.js";
|
|
12
11
|
import { Dns } from "../../../.sst/platform/src/components/dns.js";
|
|
13
|
-
import { Postgres } from "../../../.sst/platform/src/components/aws/postgres.js";
|
|
14
|
-
import { Redis } from "../../../.sst/platform/src/components/aws/redis.js";
|
|
15
|
-
import { Email } from "../../../.sst/platform/src/components/aws/email.js";
|
|
16
12
|
import { applyLinkedResourcesEnv, EnvCallback, EnvCallbacks } from "./src/laravel-env.js";
|
|
17
13
|
|
|
18
14
|
// duplicate from cluster.ts
|
|
@@ -164,6 +160,8 @@ export interface LaravelArgs extends ClusterArgs {
|
|
|
164
160
|
}
|
|
165
161
|
|
|
166
162
|
export class Laravel extends Component {
|
|
163
|
+
private readonly services: Record<string, sst.aws.Service>;
|
|
164
|
+
|
|
167
165
|
constructor(
|
|
168
166
|
name: string,
|
|
169
167
|
args: LaravelArgs,
|
|
@@ -171,13 +169,15 @@ export class Laravel extends Component {
|
|
|
171
169
|
) {
|
|
172
170
|
super(__pulumiType, name, args, opts);
|
|
173
171
|
|
|
172
|
+
this.services = {};
|
|
173
|
+
|
|
174
174
|
args.config = args.config ?? {};
|
|
175
175
|
const sitePath = args.path ?? '.';
|
|
176
176
|
const absSitePath = path.resolve(sitePath.toString());
|
|
177
|
-
|
|
178
|
-
const nodeModulePath = path.resolve(__dirname, '../../node_modules/sst-laravel');
|
|
177
|
+
const nodeModulePath = path.resolve(__dirname, '../../node_modules/@kirschbaum-development/sst-laravel');
|
|
179
178
|
|
|
180
|
-
// Determine the path where our plugin will save build files.
|
|
179
|
+
// Determine the path where our plugin will save build files.
|
|
180
|
+
// SST sets __dirname to the .sst/platform directory.
|
|
181
181
|
const pluginBuildPath = path.resolve(__dirname, '../laravel');
|
|
182
182
|
|
|
183
183
|
prepareEnvironmentFile();
|
|
@@ -187,18 +187,10 @@ export class Laravel extends Component {
|
|
|
187
187
|
vpc: args.vpc
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
addWebService();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (args.workers) {
|
|
195
|
-
addWorkerServices();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function addWebService() {
|
|
190
|
+
const addWebService = () => {
|
|
199
191
|
const envVariables = getEnvironmentVariables();
|
|
200
192
|
|
|
201
|
-
|
|
193
|
+
this.services['web'] = new sst.aws.Service(`${name}-Web`, {
|
|
202
194
|
cluster,
|
|
203
195
|
link: getLinks(),
|
|
204
196
|
permissions: args.permissions,
|
|
@@ -257,7 +249,7 @@ export class Laravel extends Component {
|
|
|
257
249
|
});
|
|
258
250
|
}
|
|
259
251
|
|
|
260
|
-
|
|
252
|
+
const createWorkerService = (workerConfig: LaravelWorkerConfig, serviceName: string, workerBuildPath: string) => {
|
|
261
253
|
createWorkerTasks(workerConfig, workerBuildPath);
|
|
262
254
|
|
|
263
255
|
const imgBuildArgs = {
|
|
@@ -265,7 +257,7 @@ export class Laravel extends Component {
|
|
|
265
257
|
'CUSTOM_CONF_PATH': workerBuildPath.replace(absSitePath, ''),
|
|
266
258
|
};
|
|
267
259
|
|
|
268
|
-
|
|
260
|
+
this.services[serviceName] = new sst.aws.Service(serviceName, {
|
|
269
261
|
cluster,
|
|
270
262
|
link: getLinks(),
|
|
271
263
|
permissions: args.permissions,
|
|
@@ -304,6 +296,14 @@ export class Laravel extends Component {
|
|
|
304
296
|
});
|
|
305
297
|
}
|
|
306
298
|
|
|
299
|
+
if (args.web) {
|
|
300
|
+
addWebService();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (args.workers) {
|
|
304
|
+
addWorkerServices();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
307
|
function getDefaultPublicPorts(): Ports {
|
|
308
308
|
let ports;
|
|
309
309
|
const forwardPort: Port = "8080/http";
|
|
@@ -324,7 +324,7 @@ export class Laravel extends Component {
|
|
|
324
324
|
return ports;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
// TODO: We have to test if it works when
|
|
327
|
+
// TODO: We have to test if it works when a custom image is provided in sst.config.js
|
|
328
328
|
function getImage(imgFromConfig: LaravelWebArgs["image"] | null | undefined, imgType: ImageType, extraArgs: object = {}) {
|
|
329
329
|
const img = imgFromConfig
|
|
330
330
|
? imgFromConfig
|
|
@@ -380,6 +380,7 @@ export class Laravel extends Component {
|
|
|
380
380
|
'PHP_OPCACHE_ENABLE': args.config?.opcache? '1' : '0',
|
|
381
381
|
'AUTORUN_LARAVEL_MIGRATION': imageType === ImageType.Web ? 'true' : 'false',
|
|
382
382
|
'CONTAINER_TYPE': imageType,
|
|
383
|
+
'ENV_FILENAME': args.config?.environment?.file ?? '.env',
|
|
383
384
|
stage: "deploy",
|
|
384
385
|
platform: "linux/amd64",
|
|
385
386
|
...extraArgs
|
|
@@ -493,6 +494,16 @@ export class Laravel extends Component {
|
|
|
493
494
|
fs.chmodSync(dst, 0o755);
|
|
494
495
|
}
|
|
495
496
|
};
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* The URL of the service.
|
|
500
|
+
*
|
|
501
|
+
* If `public.domain` is set, this is the URL with the custom domain.
|
|
502
|
+
* Otherwise, it's the auto-generated load balancer URL.
|
|
503
|
+
*/
|
|
504
|
+
public get url() {
|
|
505
|
+
return this.services['web'].url;
|
|
506
|
+
}
|
|
496
507
|
}
|
|
497
508
|
|
|
498
509
|
const __pulumiType = "sst:aws:Laravel";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirschbaum-development/sst-laravel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "An unofficial extension of SST to deploy containerized Laravel applications to AWS Fargate.",
|
|
5
5
|
"main": "laravel-sst.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -25,5 +25,16 @@
|
|
|
25
25
|
"url": "https://github.com/kirschbaum-development/sst-laravel/issues"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/kirschbaum-development/sst-laravel#readme",
|
|
28
|
-
"
|
|
28
|
+
"bin": {
|
|
29
|
+
"sst-laravel": "./bin/cli.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@aws-sdk/client-ecs": "^3.0.0",
|
|
33
|
+
"@inquirer/prompts": "^7.0.0",
|
|
34
|
+
"commander": "^12.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
}
|
|
29
40
|
}
|
package/sst-env.d.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/// <reference path="./.sst/platform/config.d.ts" />
|
|
2
|
+
|
|
3
|
+
export default $config({
|
|
4
|
+
app(input) {
|
|
5
|
+
return {
|
|
6
|
+
name: "my-laravel-app",
|
|
7
|
+
removal: input?.stage === "production" ? "retain" : "remove",
|
|
8
|
+
protect: ["production"].includes(input?.stage),
|
|
9
|
+
home: "aws",
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
async run() {
|
|
13
|
+
const { Laravel } = await import("@kirschbaum-development/sst-laravel");
|
|
14
|
+
const vpc = new sst.aws.Vpc("MyVpc");
|
|
15
|
+
|
|
16
|
+
const app = new Laravel("MyLaravelApp", {
|
|
17
|
+
vpc,
|
|
18
|
+
|
|
19
|
+
web: {
|
|
20
|
+
domain: $app.stage === "production"
|
|
21
|
+
? "example.com"
|
|
22
|
+
: `${$app.stage}.example.com`,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
workers: [{
|
|
26
|
+
horizon: true,
|
|
27
|
+
scheduler: true,
|
|
28
|
+
}]
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
url: app.url,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|