@kirschbaum-development/sst-laravel 0.0.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/.dockerignore ADDED
@@ -0,0 +1,5 @@
1
+ # sst
2
+ .sst/**
3
+ !.sst/laravel
4
+ node_modules
5
+ .env
package/.editorconfig ADDED
@@ -0,0 +1,18 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_size = 2
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
13
+
14
+ [*.{yml,yaml}]
15
+ indent_size = 2
16
+
17
+ [docker-compose.yml]
18
+ indent_size = 4
package/Dockerfile.web ADDED
@@ -0,0 +1,57 @@
1
+ ARG PHP_VERSION=8.3
2
+ FROM serversideup/php:${PHP_VERSION}-unit as base
3
+
4
+ USER root
5
+
6
+ LABEL authors="Kirschbaum"
7
+ LABEL maintainer="Kirschbaum"
8
+
9
+ ENV AUTORUN_ENABLED=false
10
+ # ENV AUTORUN_LARAVEL_MIGRATION=true
11
+
12
+ RUN apt-get update \
13
+ && apt-get upgrade -y
14
+
15
+ RUN install-php-extensions gd intl
16
+
17
+ # ENV NVM_VERSION v0.39.7
18
+ # ENV NODE_VERSION 21.6.0
19
+ # ENV NVM_DIR /usr/local/nvm
20
+ # RUN mkdir "$NVM_DIR"
21
+
22
+ # RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
23
+
24
+ # ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
25
+ # ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
26
+
27
+ # RUN echo "source $NVM_DIR/nvm.sh \
28
+ # && nvm install $NODE_VERSION \
29
+ # && nvm alias default $NODE_VERSION \
30
+ # && nvm use default \
31
+ # && nvm install-latest-npm" | bash
32
+
33
+ FROM base as development
34
+
35
+ # Save the build arguments as a variable
36
+ ARG USER_ID
37
+ ARG GROUP_ID
38
+
39
+ # Drop back to our unprivileged user
40
+ USER www-data
41
+
42
+ FROM base as deploy
43
+
44
+ ARG ENV_FILENAME
45
+ ENV CONTAINER_TYPE=web
46
+
47
+ # Enable OPCache
48
+ ENV PHP_OPCACHE_ENABLE=1
49
+
50
+ COPY --chown=www-data:www-data . /var/www/html
51
+ COPY --chown=www-data:www-data $ENV_FILENAME /var/www/html/.env
52
+
53
+ # Copy deploy script generated into .sst/laravel by the build step
54
+ COPY --chmod=755 .sst/laravel/deploy/60-deploy.sh /etc/entrypoint.d/60-deploy.sh
55
+
56
+ # Drop back to our unprivileged user
57
+ USER www-data
@@ -0,0 +1,85 @@
1
+ ARG PHP_VERSION=8.3
2
+ FROM serversideup/php:${PHP_VERSION}-cli AS phpcli
3
+
4
+ ##########
5
+ # S6 Build
6
+ ##########
7
+ FROM phpcli AS s6-build
8
+
9
+ ARG CONF_PATH
10
+
11
+ USER root
12
+
13
+ ARG S6_DIR='/opt/s6/'
14
+ ARG S6_SRC_URL="https://github.com/just-containers/s6-overlay/releases/download"
15
+
16
+ # copy our scripts
17
+ COPY --chmod=755 $CONF_PATH /
18
+
19
+ RUN s6-install.sh
20
+
21
+ ############
22
+ # BASE IMAGE
23
+ ############
24
+ FROM phpcli AS base
25
+
26
+ ARG CONF_PATH
27
+ ARG CUSTOM_CONF_PATH
28
+
29
+ USER root
30
+
31
+ LABEL authors="Kirschbaum"
32
+ LABEL maintainer="Kirschbaum"
33
+
34
+ ENV AUTORUN_ENABLED=false
35
+ ENV AUTORUN_LARAVEL_MIGRATION=false
36
+
37
+ # copy our scripts
38
+ RUN test -n "$CUSTOM_CONF_PATH"
39
+
40
+ COPY --chmod=755 $CONF_PATH /
41
+ COPY --chmod=755 $CUSTOM_CONF_PATH /
42
+
43
+ # copy s6-overlay from s6-build
44
+ COPY --from=s6-build /opt/s6/ /
45
+
46
+ RUN apt-get update \
47
+ && apt-get upgrade -y
48
+
49
+ # RUN install-php-extensions gd intl
50
+
51
+ ###################
52
+ # Development image
53
+ ###################
54
+ FROM base AS development
55
+
56
+ # Save the build arguments as a variable
57
+ ARG USER_ID
58
+ ARG GROUP_ID
59
+
60
+ # Drop back to our unprivileged user
61
+ USER www-data
62
+
63
+ ##############
64
+ # Deploy image
65
+ ##############
66
+ FROM base AS deploy
67
+
68
+ ENV CONTAINER_TYPE=cli
69
+
70
+ COPY --chown=www-data:www-data . /var/www/html
71
+
72
+ # Fix S6 Overlay issues with Big Cloud PaaS (https://github.com/serversideup/docker-php/pull/376#issuecomment-2179262427)
73
+ RUN chown -R www-data:www-data /run
74
+
75
+ # Copy deploy script generated into .sst/laravel by the build step
76
+ COPY --chmod=755 .sst/laravel/deploy/60-deploy.sh /etc/entrypoint.d/60-deploy.sh
77
+
78
+ USER www-data
79
+
80
+ ENTRYPOINT ["entrypoint.sh"]
81
+
82
+ # Set stop signal to SIGQUIT for a graceful shutdown instead of S6's preferred SIGTERM (https://github.com/just-containers/s6-overlay/issues/586)
83
+ STOPSIGNAL SIGQUIT
84
+
85
+ CMD ["/init"]
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # SST Laravel
2
+
3
+ This is an unofficial extension of SST to deploy your Laravel application to AWS behind a robust, reliable and scalable infrastructure, with all the power the SST provides.
4
+
5
+ **TODO: Add explanation about what exactly SST is.**
6
+
7
+ ## What it deploys
8
+
9
+ Behind the scenes, this extension uses the SST Cluster + Service component, which runs in AWS Fargate using pre-built Docker containers. This all gets deployed on your own AWS account, and you have full control over the infrastructure. Behind the scenes, we use the powerful PHP containers from Serverside Up.
10
+
11
+ This package deploys a full-blown infrastructure in AWS, with zero downtime deployments, as it can be seeing in the image below.
12
+
13
+ ![](./images/diagram.png)
14
+
15
+ ## Pre-requisites
16
+
17
+ 1. NodeJS.
18
+ 1. Have [SST](https://sst.dev) installed and configured.
19
+
20
+ ## Installation instructions
21
+
22
+ Pull in the package using npm:
23
+
24
+ ```bash
25
+ npm install @kirschbaum/sst-laravel --save
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ To start using, you only need to import the component in your `sst.config.ts` file:
31
+
32
+ ```ts
33
+ import { Laravel } from "@kirschbaum-development/sst-laravel";
34
+ ```
35
+
36
+ And now you can start using the `Laravel` SST component. All the configuration options are Typescript files with documentation, so
37
+
38
+ // TODO: Add full list of options (auto generate?)
39
+ To check the full list of options. check [here]().
40
+
41
+ ### HTTP
42
+
43
+ Setting up your app to receive HTTP requests, on the `laravel-sst-demo.kdg.dev` domain (with SSL), with auto-scaling with a max of 3 servers.
44
+
45
+ ```js
46
+ const app = new Laravel('MyLaravelApp', {
47
+ web: {
48
+ domain: 'laravel-sst-demo.kdg.dev',
49
+ scaling: {
50
+ min: 1,
51
+ max: 3,
52
+ }
53
+ },
54
+ });
55
+ ```
56
+
57
+ ### Workers
58
+
59
+ Beyond HTTP requests, you can set up one or more `workers` for your Laravel application. Workers are meant to run background commands like Laravel Horizon, the Laravel Scheduler or any background command you may need to run.
60
+
61
+ SST Laravel will automatically deploy and configure worker containers running your configured commands. See some examples below.
62
+
63
+
64
+ **Running the Laravel scheduler**
65
+
66
+ ```js
67
+ const app = new Laravel('MyLaravelApp', {
68
+ workers: [
69
+ {
70
+ name: 'scheduler',
71
+ scheduler: true,
72
+ },
73
+ ],
74
+ });
75
+ ```
76
+
77
+ **Running the Laravel Horizon**
78
+
79
+ ```js
80
+ const app = new Laravel('MyLaravelApp', {
81
+ workers: [
82
+ {
83
+ name: 'horizon',
84
+ horizon: true,
85
+ },
86
+ ],
87
+ });
88
+ ```
89
+
90
+ **Running custom commands**
91
+
92
+ ```js
93
+ const app = new Laravel('MyLaravelApp', {
94
+ workers: [
95
+ {
96
+ name: 'worker',
97
+ tasks: {
98
+ 'scheduler': {
99
+ command: 'php artisan schedule:work',
100
+ },
101
+ 'queue': {
102
+ command: 'php artisan queue:work',
103
+ },
104
+ 'pulse': {
105
+ command: 'php artisan pulse:work',
106
+ },
107
+ },
108
+ },
109
+ ],
110
+ });
111
+ ```
112
+
113
+ ## Environment Variables
114
+
115
+ There are multiple ways to configure environment variables. If you want SST Laravel to copy an environment file, you can configure the `config.environment.file` entry.
116
+
117
+ The below configuration would copy a file named `.env.$STAGE` into the deployment containers as your `.env` file.
118
+
119
+ ```js
120
+ const app = new Laravel('MyLaravelApp', {
121
+ // ...
122
+ config: {
123
+ environment: {
124
+ file: `.env.${$app.stage}`,
125
+ }
126
+ }
127
+ });
128
+ ```
129
+
130
+ You can also configure it to use simply `.env`.
131
+
132
+ ```js
133
+ const app = new Laravel('MyLaravelApp', {
134
+ // ...
135
+ config: {
136
+ environment: {
137
+ file: `.env`,
138
+ }
139
+ }
140
+ });
141
+ ```
142
+
143
+ ### Resources
144
+
145
+ In SST, you can [link resources](https://sst.dev/docs/linking). If you link resources to your Laravel component, SST Laravel will automatically inject and configure environment variables using sensible defaults for all the linked resources.
146
+
147
+ In the example configuration below, SST Laravel will automatically inject environment variables for the database, cache and filesystem.
148
+
149
+ ```js
150
+ const database = new sst.aws.Postgres('MyDatabase', { vpc });
151
+ const redis = new sst.aws.Redis("MyRedis", { vpc });
152
+ const bucket = new sst.aws.Bucket("MyBucket");
153
+
154
+ const app = new Laravel('MyLaravelApp', {
155
+ link: [database, redis, bucket],
156
+ });
157
+ ```
158
+
159
+ The `DB_*`, `REDIS_*` and `AWS_*` environment variables will be automatically injected into your Laravel application.
160
+
161
+ #### Custom Environment Key Names
162
+
163
+ If you need to customize the environment variable names for your resources, you can provide an object with the resource and a callback function in the `link` array:
164
+
165
+ ```js
166
+ const app = new Laravel('MyLaravelApp', {
167
+ link: [
168
+ email,
169
+ {
170
+ resource: database,
171
+ environment: (database: sst.aws.Postgres) => ({
172
+ CUSTOM_DB_HOST: database.host.apply(host => host.toString()),
173
+ CUSTOM_DB_NAME: database.database.apply(database => database.toString()),
174
+ CUSTOM_DB_USER: database.username.apply(username => username.toString()),
175
+ CUSTOM_DB_PASSWORD: database.password.apply(password => password.toString()),
176
+ })
177
+ },
178
+ {
179
+ resource: redis,
180
+ environment: (redis: sst.aws.Redis) => ({
181
+ QUEUE_CONNECTION: 'redis',
182
+ QUEUE_REDIS_HOST: redis.host.apply(host => host ? `tls://${host}` : ''),
183
+ QUEUE_REDIS_PORT: redis.port.apply(port => port.toString()),
184
+ })
185
+ }
186
+ ],
187
+ web: {}
188
+ });
189
+ ```
190
+
191
+ The callback function receives the resource as a parameter and should return an object with the custom environment variables. The default environment variables are still set, so you can either override them or add new ones.
192
+
193
+ #### Disabling the auto-inject of environment variables
194
+
195
+ If you don't want SST Laravel to auto-inject environment variables, you can disable with the following option:
196
+
197
+ ```js
198
+ config: {
199
+ environment: {
200
+ autoInject: false,
201
+ }
202
+ }
203
+ ```
204
+
205
+ #### IAM Roles and Permissions
206
+
207
+ The IAM permissions for the linked resources are also automatically added to the ECS IAM Execution Role, meaning your application has access to all the linked resources.
208
+
209
+ ### Other Configurations
210
+
211
+ You can configure the PHP version, custom environment variables and a custom deployment script.
212
+
213
+ ```js
214
+ const app = new Laravel('MyLaravelApp', {
215
+ config: {
216
+ php: 8.4,
217
+ opcache: true,
218
+ deployment: {
219
+ script: './infra/deploy.sh'
220
+ },
221
+ },
222
+ });
223
+ ```
224
+
225
+ Custom deployment script example:
226
+
227
+ ```bash
228
+ #!/bin/sh
229
+
230
+ # Exit on error
231
+ set -e
232
+
233
+ echo "🚀 Running Deployment Script..."
234
+
235
+ cd "$APP_BASE_DIR"
236
+
237
+ echo "🚀 Running PHP Artisan Optimize..."
238
+ php artisan optimize
239
+
240
+ echo "🚀 Running Laravel Migrations..."
241
+ php artisan migrate --force
242
+ ```
243
+
244
+ ## Debugging Containers
245
+
246
+ TODO: Add documentation on how to SSH to debug containers.
247
+
248
+ ***
249
+
250
+ ### Roadmap
251
+
252
+ * Custom CLI to facilitate accessing resources;
253
+ * Add support for Inertia SSR;
254
+ * Add support for Octane;
255
+ * Add support for Laravel Reverb;
256
+ * Dev mode;
257
+
258
+ ## Security
259
+
260
+ If you discover any security related issues, please email security@kirschbaumdevelopment.com instead of using the issue tracker.
261
+
262
+ ## Sponsorship
263
+
264
+ Development of this package is sponsored by Kirschbaum Development Group, a developer driven company focused on problem solving, team building, and community. Learn more [about us](https://kirschbaumdevelopment.com) or [join us](https://careers.kirschbaumdevelopment.com)!
265
+
266
+ ## License
267
+
268
+ The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ echo "KIRSCHBAUM DEVELOPMENT GROUP"
@@ -0,0 +1,4 @@
1
+ #!/command/execlineb -S0
2
+ php artisan horizon:terminate
3
+ echo "0" > /run/s6-linux-init-container-results/exitcode
4
+ /run/s6/basedir/bin/halt
@@ -0,0 +1,16 @@
1
+ #!/command/with-contenv bash
2
+
3
+ # Exit on error
4
+ set -e
5
+
6
+ # Check to see if an Artisan file exists and assume it means Laravel is configured.
7
+ if [ -f "$APP_BASE_DIR/artisan" ]; then
8
+ echo "🚀 Starting Laravel Horizon..."
9
+
10
+ cd "$APP_BASE_DIR"
11
+ php "$APP_BASE_DIR/artisan" horizon
12
+ else
13
+ echo "👉 Skipping Laravel Horizon because we could not detect a Laravel install or it was specifically disabled..."
14
+
15
+ tail -f /dev/null
16
+ fi
@@ -0,0 +1,2 @@
1
+ #!/command/execlineb -P
2
+ /etc/s6-overlay/s6-rc.d/laravel-horizon/laravel-horizon
@@ -0,0 +1,4 @@
1
+ #!/command/execlineb -S0
2
+ php artisan schedule:interrupt
3
+ echo "0" > /run/s6-linux-init-container-results/exitcode
4
+ /run/s6/basedir/bin/halt
@@ -0,0 +1,16 @@
1
+ #!/command/with-contenv bash
2
+
3
+ # Exit on error
4
+ set -e
5
+
6
+ # Check to see if an Artisan file exists and assume it means Laravel is configured.
7
+ if [ -f "$APP_BASE_DIR/artisan" ]; then
8
+ echo "🚀 Starting Laravel Scheduler..."
9
+
10
+ cd "$APP_BASE_DIR"
11
+ php "$APP_BASE_DIR/artisan" schedule:work
12
+ else
13
+ echo "👉 Skipping Laravel Scheduler because we could not detect a Laravel install or it was specifically disabled..."
14
+
15
+ tail -f /dev/null
16
+ fi
@@ -0,0 +1,2 @@
1
+ #!/command/execlineb -P
2
+ /etc/s6-overlay/s6-rc.d/laravel-scheduler/laravel-scheduler
@@ -0,0 +1,76 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ ##############
5
+ # BASED ON https://github.com/serversideup/docker-php/blob/main/src/common/usr/local/bin/docker-php-serversideup-entrypoint
6
+ ##############
7
+
8
+ # Initialize variables
9
+ DEFAULT_COMMAND="false"
10
+ S6_INITIALIZED="false"
11
+
12
+ echo "🔥🔥🔥 STARTING ENTRYPOINT SCRIPT 🔥🔥🔥"
13
+
14
+ # Enable debug mode if LOG_OUTPUT_LEVEL is set to "debug"
15
+ if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then
16
+ echo "🔥🔥🔥 DEBUG MODE has been set. Get ready for a ton of debug log output..."
17
+ set -x
18
+ fi
19
+
20
+ # Check if the default command is being used
21
+ case "$1" in
22
+ "/init")
23
+ DEFAULT_COMMAND="true"
24
+ ;;
25
+ "unitd")
26
+ if [ "$2" = "--no-daemon" ]; then
27
+ DEFAULT_COMMAND="true"
28
+ fi
29
+ ;;
30
+ esac
31
+
32
+ # Check if S6 overlay is initialized
33
+ if [ -d "/etc/s6-overlay" ] && [ "$DEFAULT_COMMAND" = "true" ]; then
34
+ S6_INITIALIZED="true"
35
+ fi
36
+
37
+ # Export variables
38
+ export DEFAULT_COMMAND
39
+ export S6_INITIALIZED
40
+
41
+ ###############################################
42
+ # Usage: entrypoint.sh
43
+ ###############################################
44
+ # This script is used to execute scripts from "/etc/entrypoint.d" and then
45
+ # execute the CMD passed in from the Dockerfile.
46
+
47
+ # Execute scripts from /etc/entrypoint.d/ in numeric order
48
+ find /etc/entrypoint.d/ -type f -name '*.sh' | sort -V | while IFS= read -r f; do
49
+ if [ -e "$f" ]; then
50
+ if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then
51
+ echo "Executing $f"
52
+ fi
53
+ if ! . "$f"; then
54
+ echo "Error executing $f" >&2
55
+ exit 1
56
+ fi
57
+ else
58
+ echo "Warning: $f not found" >&2
59
+ fi
60
+ done
61
+
62
+ # first arg is `-f` or `--some-option`
63
+ if [ "${1#-}" != "$1" ]; then
64
+ set -- php "$@"
65
+ fi
66
+
67
+ # Some scripts may need to change the CMD based on the log level. If this file is set, execute the contents of that file instead of the Dockerfile CMD.
68
+ if [ -f /tmp/docker_cmd_override ]; then
69
+ docker_cmd_override=$(cat /tmp/docker_cmd_override)
70
+ rm /tmp/docker_cmd_override
71
+ set -- $docker_cmd_override # Perform word splitting by not quoting the commands
72
+ exec "$@"
73
+ else
74
+ # Execute the CMD passed in from the Dockerfile
75
+ exec "$@"
76
+ fi
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+ set -oue
3
+
4
+ ######################
5
+ # Usage: s6-install.sh
6
+ ######################
7
+
8
+ # BASED ON https://github.com/serversideup/docker-php/blob/main/src/s6/usr/local/bin/docker-php-serversideup-s6-install
9
+ # This script is used to install S6 Overlay. It is intended to be used during the build process only.
10
+ # Be sure to set the S6_SRC_URL, S6_SRC_DEP, and S6_DIR environment variables before running this script.
11
+
12
+ S6_VERSION=v3.2.0.2
13
+ mkdir -p $S6_DIR
14
+ export SYS_ARCH=$(uname -m)
15
+ case "$SYS_ARCH" in
16
+ aarch64 ) export S6_ARCH='aarch64' ;;
17
+ arm64 ) export S6_ARCH='aarch64' ;;
18
+ armhf ) export S6_ARCH='armhf' ;;
19
+ arm* ) export S6_ARCH='arm' ;;
20
+ i4* ) export S6_ARCH='i486' ;;
21
+ i6* ) export S6_ARCH='i686' ;;
22
+ s390* ) export S6_ARCH='s390x' ;;
23
+ * ) export S6_ARCH='x86_64' ;;
24
+ esac
25
+
26
+ untar() {
27
+ echo "⏬ Downloading $1"
28
+ curl -L $1 -o - | tar Jxp -C $S6_DIR
29
+ }
30
+
31
+ echo "⬇️ Downloading s6 overlay:${S6_ARCH}-${S6_VERSION} for ${SYS_ARCH}"
32
+ untar ${S6_SRC_URL}/${S6_VERSION}/s6-overlay-noarch.tar.xz
33
+ untar ${S6_SRC_URL}/${S6_VERSION}/s6-overlay-${S6_ARCH}.tar.xz
Binary file
package/laravel-sst.ts ADDED
@@ -0,0 +1,500 @@
1
+ /// <reference path="./../../.sst/platform/config.d.ts" />
2
+
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { Component } from "../../.sst/platform/src/components/component.js";
6
+ import { FunctionArgs } from "../../.sst/platform/src/components/aws/function.js";;
7
+ import { ComponentResourceOptions, Output, all, output } from "../../.sst/platform/node_modules/@pulumi/pulumi/index.js";
8
+ import { Input } from "../../.sst/platform/src/components/input.js";
9
+ import { Link } from "../../.sst/platform/src/components/link.js";
10
+ import { ClusterArgs } from "../../.sst/platform/src/components/aws/cluster.js";
11
+ import { ServiceArgs } from "../../.sst/platform/src/components/aws/service.js";
12
+ 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
+ import { applyLinkedResourcesEnv, EnvCallback, EnvCallbacks } from "./src/laravel-env.js";
17
+
18
+ // duplicate from cluster.ts
19
+ type Port = `${number}/${"http" | "https" | "tcp" | "udp" | "tcp_udp" | "tls"}`;
20
+
21
+ type Ports = {
22
+ listen: Port,
23
+ forward: Port
24
+ }[];
25
+
26
+ enum ImageType {
27
+ Web = 'web',
28
+ Worker = 'worker',
29
+ Cli = 'cli',
30
+ }
31
+
32
+ export interface LaravelWebArgs {
33
+ /**
34
+ * Domain for the web layer.
35
+ */
36
+ domain?: Input<
37
+ string
38
+ | {
39
+ name: Input<string>;
40
+ cert?: Input<string>;
41
+ dns?: Input<false | (Dns & {})>;
42
+ }
43
+ >;
44
+
45
+ loadBalancer?: ServiceArgs["loadBalancer"];
46
+ image?: ServiceArgs["image"];
47
+ scaling?: ServiceArgs["scaling"];
48
+ }
49
+
50
+ export interface LaravelWorkerConfig {
51
+ name?: Input<string>;
52
+ link?: ServiceArgs["link"];
53
+ scaling?: ServiceArgs["scaling"];
54
+
55
+ /**
56
+ * Running horizon?
57
+ */
58
+ horizon?: Input<boolean>;
59
+
60
+ /**
61
+ * Running scheduler?
62
+ */
63
+ scheduler?: Input<boolean>;
64
+
65
+ /**
66
+ * Multiple tasks can be run in the worker.
67
+ */
68
+ tasks?: Input<{
69
+ [key: string]: Input<{
70
+ command: Input<string>;
71
+ dependencies?: Input<string[]>;
72
+ }>
73
+ }>
74
+ }
75
+
76
+ export interface LaravelArgs extends ClusterArgs {
77
+ // dev?: false | DevArgs["dev"];
78
+ path?: Input<string>;
79
+ link?: Array<
80
+ | any
81
+ | {
82
+ resource: any;
83
+ environment?: EnvCallback;
84
+ }
85
+ >;
86
+
87
+ permissions?: Array<{
88
+ actions: string[];
89
+ resources: string[];
90
+ }>;
91
+
92
+ /**
93
+ * If enabled, a container will be created to handle HTTP traffic.
94
+ */
95
+ web?: LaravelWebArgs;
96
+
97
+ /**
98
+ * Multiple workers settings.
99
+ */
100
+ workers?: LaravelWorkerConfig[];
101
+
102
+ /**
103
+ * Config settings.
104
+ */
105
+ config?: {
106
+ php?: Input<Number>;
107
+
108
+ /**
109
+ * PHP Opcache should be enabled?
110
+ *
111
+ * @default `true`
112
+ */
113
+ opcache?: Input<boolean>;
114
+ environment?: {
115
+ /**
116
+ * Use this option if you want to import an .env file during build. By default, SST Laravel won't use your .env file since that might be the wrong file when deploying from your local machine.
117
+ *
118
+ * @example
119
+ * ```js
120
+ * # Use use a fila named .env.$stage as your .env file
121
+ * environment: {
122
+ * file: `.env.${$app.stage}`,
123
+ * }
124
+ * OR
125
+ * environment: {
126
+ * file: `.env`,
127
+ * }
128
+ * ```
129
+ */
130
+ file?: Input<string>,
131
+
132
+ /**
133
+ * Set this to false in case you don't want to auto inject environment variables from your linked resources.
134
+ *
135
+ * @default `true`
136
+ */
137
+ autoInject?: Input<boolean>,
138
+
139
+ /**
140
+ * Custom environment variables that will be automatically injected into your application.
141
+ *
142
+ * @example
143
+ * ```js
144
+ * environment: {
145
+ * vars: {
146
+ * SESSION_DRIVER: 'redis',
147
+ * QUEUE_CONNECTION: 'redis',
148
+ * }
149
+ * }
150
+ * ```
151
+ */
152
+ vars?: FunctionArgs["environment"],
153
+ };
154
+
155
+ /**
156
+ * Custom deployment configurations.
157
+ */
158
+ deployment?: {
159
+ migrate?: Input<boolean>;
160
+ optimize?: Input<boolean>;
161
+ script?: Input<string>;
162
+ };
163
+ }
164
+ }
165
+
166
+ export class Laravel extends Component {
167
+ constructor(
168
+ name: string,
169
+ args: LaravelArgs,
170
+ opts: ComponentResourceOptions = {},
171
+ ) {
172
+ super(__pulumiType, name, args, opts);
173
+
174
+ args.config = args.config ?? {};
175
+ const sitePath = args.path ?? '.';
176
+ const absSitePath = path.resolve(sitePath.toString());
177
+ // TODO: We need to update sst-laravel to whatever the real package name will be.
178
+ const nodeModulePath = path.resolve(__dirname, '../../node_modules/sst-laravel');
179
+
180
+ // Determine the path where our plugin will save build files. SST sets __dirname to the .sst/platform directory.
181
+ const pluginBuildPath = path.resolve(__dirname, '../laravel');
182
+
183
+ prepareEnvironmentFile();
184
+ prepareDeploymentScript();
185
+
186
+ const cluster = new sst.aws.Cluster(`${name}-Cluster`, {
187
+ vpc: args.vpc
188
+ });
189
+
190
+ if (args.web) {
191
+ addWebService();
192
+ }
193
+
194
+ if (args.workers) {
195
+ addWorkerServices();
196
+ }
197
+
198
+ function addWebService() {
199
+ const envVariables = getEnvironmentVariables();
200
+
201
+ const webService = new sst.aws.Service(`${name}-Web`, {
202
+ cluster,
203
+ link: getLinks(),
204
+ permissions: args.permissions,
205
+
206
+ /**
207
+ * Image passed or use our default provided image.
208
+ */
209
+ image: getImage(args.web?.image, ImageType.Web),
210
+ environment: envVariables,
211
+ scaling: args.web?.scaling,
212
+
213
+ loadBalancer: args.web && args.web.loadBalancer ? args.web.loadBalancer : {
214
+ domain: args.web?.domain,
215
+ ports: getDefaultPublicPorts(),
216
+ },
217
+
218
+ dev: {
219
+ command: `php ${sitePath}/artisan serve`,
220
+ },
221
+ });
222
+ }
223
+
224
+ function createWorkerTasks(workerConfig: LaravelWorkerConfig, workerBuildPath: string) {
225
+ const s6RcDPath = path.resolve(workerBuildPath, 'etc/s6-overlay/s6-rc.d');
226
+ const s6UserContentsPath = path.resolve(s6RcDPath, 'user/contents.d');
227
+
228
+ fs.mkdirSync(s6UserContentsPath, { recursive: true });
229
+
230
+ const tasks: Record<string, { command: string; dependencies?: string[] }> = {
231
+ ...((workerConfig.tasks as any) ?? {}),
232
+ };
233
+
234
+ if (workerConfig.horizon) {
235
+ tasks['laravel-horizon'] = {
236
+ command: 'php artisan horizon',
237
+ };
238
+ }
239
+
240
+ if (workerConfig.scheduler) {
241
+ tasks['laravel-scheduler'] = {
242
+ command: 'php artisan schedule:work',
243
+ };
244
+ }
245
+
246
+ Object.entries(tasks).forEach(([taskName, config]) => {
247
+ const tasksDir = path.resolve(s6RcDPath, `${taskName}`);
248
+ fs.mkdirSync(tasksDir, { recursive: true });
249
+
250
+ const scriptSrcPath = path.join(tasksDir, 'script');
251
+
252
+ fs.writeFileSync(scriptSrcPath, `#!/command/with-contenv bash\ncd /var/www/html\n${config.command}`, { mode: 0o777 });
253
+ fs.writeFileSync(path.join(tasksDir, 'run'), `#!/command/execlineb -P\n/etc/s6-overlay/s6-rc.d/${taskName}/script`, { mode: 0o777 });
254
+ fs.writeFileSync(path.join(tasksDir, 'type'), 'longrun');
255
+ fs.writeFileSync(path.join(tasksDir, 'dependencies'), (config.dependencies || []).join('\n'));
256
+ fs.writeFileSync(path.join(s6UserContentsPath, taskName), '');
257
+ });
258
+ }
259
+
260
+ function createWorkerService(workerConfig: LaravelWorkerConfig, serviceName: string, workerBuildPath: string) {
261
+ createWorkerTasks(workerConfig, workerBuildPath);
262
+
263
+ const imgBuildArgs = {
264
+ 'CONF_PATH': path.resolve(nodeModulePath, 'conf').replace(absSitePath, ''),
265
+ 'CUSTOM_CONF_PATH': workerBuildPath.replace(absSitePath, ''),
266
+ };
267
+
268
+ return new sst.aws.Service(serviceName, {
269
+ cluster,
270
+ link: getLinks(),
271
+ permissions: args.permissions,
272
+
273
+ image: getImage(args.web?.image, ImageType.Worker, imgBuildArgs),
274
+ scaling: workerConfig.scaling,
275
+ environment: getEnvironmentVariables(),
276
+
277
+ dev: {
278
+ command: `php ${sitePath}/artisan horizon`,
279
+ },
280
+
281
+ transform: {
282
+ taskDefinition: (args) => {
283
+ args.containerDefinitions = (args.containerDefinitions as $util.Output<string>).apply(a => {
284
+ return JSON.stringify([{
285
+ ...JSON.parse(a)[0],
286
+ linuxParameters: {
287
+ initProcessEnabled: false,
288
+ }
289
+ }]);
290
+ })
291
+ }
292
+ }
293
+ }, {
294
+ dependsOn: [],
295
+ });
296
+ }
297
+
298
+ function addWorkerServices() {
299
+ args.workers?.forEach((workerConfig, index) => {
300
+ const workerName = workerConfig.name || `worker-${index + 1}`;
301
+ const absWorkerBuildPath = path.resolve(pluginBuildPath, `worker-${workerName}`);
302
+
303
+ createWorkerService(workerConfig, `${name}-${workerName}`, absWorkerBuildPath);
304
+ });
305
+ }
306
+
307
+ function getDefaultPublicPorts(): Ports {
308
+ let ports;
309
+ const forwardPort: Port = "8080/http";
310
+ const portHttp: Port = "80/http";
311
+ const portHttps: Port = "443/https";
312
+
313
+ if (args.web?.domain) {
314
+ ports = [
315
+ { listen: portHttp, forward: forwardPort },
316
+ { listen: portHttps, forward: forwardPort },
317
+ ];
318
+ } else {
319
+ ports = [
320
+ { listen: portHttp, forward: forwardPort },
321
+ ];
322
+ }
323
+
324
+ return ports;
325
+ }
326
+
327
+ // TODO: We have to test if it works when an image is provided in sst.config.js
328
+ function getImage(imgFromConfig: LaravelWebArgs["image"] | null | undefined, imgType: ImageType, extraArgs: object = {}) {
329
+ const img = imgFromConfig
330
+ ? imgFromConfig
331
+ : getDefaultImage(imgType, extraArgs);
332
+
333
+ const context = typeof img === 'string'
334
+ ? sitePath.toString()
335
+ : (img as { context: string }).context.toString();
336
+
337
+ const dockerfile = typeof img === 'string'
338
+ ? 'Dockerfile'
339
+ : (img as { dockerfile: string }).dockerfile;
340
+
341
+ // add .sst/laravel to .dockerignore if not exist
342
+ const dockerIgnore = (() => {
343
+ let filePath = path.join(context, `${dockerfile}.dockerignore`);
344
+ if (fs.existsSync(filePath)) return filePath;
345
+
346
+ filePath = path.join(context, ".dockerignore");
347
+ if (fs.existsSync(filePath)) return filePath;
348
+ })();
349
+
350
+ const content = dockerIgnore ? fs.readFileSync(dockerIgnore).toString() : "";
351
+
352
+ const lines = content.split("\n");
353
+
354
+ // SST adds it later, so we need to add it here to ensure .sst/laravel is after it and is not ignored
355
+ if (dockerIgnore) {
356
+ if (!lines.find((line) => line === ".sst")) {
357
+ fs.writeFileSync(
358
+ dockerIgnore,
359
+ [...lines, "", "# sst", "!.sst/laravel"].join("\n"),
360
+ );
361
+ }
362
+
363
+ if (!lines.find((line) => line === "!.sst/laravel")) {
364
+ fs.writeFileSync(
365
+ dockerIgnore,
366
+ [...lines, "", "# sst-laravel", "!.sst/laravel"].join("\n"),
367
+ );
368
+ }
369
+ }
370
+
371
+ return img;
372
+ }
373
+
374
+ function getDefaultImage(imageType: ImageType, extraArgs: object = {}) {
375
+ return {
376
+ context: sitePath,
377
+ dockerfile: path.resolve(nodeModulePath, `Dockerfile.${imageType}`).replace(absSitePath, '.'),
378
+ args: {
379
+ 'PHP_VERSION': getPhpVersion().toString(),
380
+ 'PHP_OPCACHE_ENABLE': args.config?.opcache? '1' : '0',
381
+ 'AUTORUN_LARAVEL_MIGRATION': imageType === ImageType.Web ? 'true' : 'false',
382
+ 'CONTAINER_TYPE': imageType,
383
+ stage: "deploy",
384
+ platform: "linux/amd64",
385
+ ...extraArgs
386
+ },
387
+ };
388
+ };
389
+
390
+ function getPhpVersion() {
391
+ return args.config?.php ?? 8.4;
392
+ }
393
+
394
+ function getEnvironmentVariables() {
395
+ const env = args.config?.environment?.vars || {};
396
+
397
+ if (args.web?.domain) {
398
+ if (typeof args.web.domain === 'string') {
399
+ (env as any)['APP_URL'] = args.web.domain;
400
+ }
401
+ }
402
+
403
+ return env;
404
+ }
405
+
406
+ function applyLinkedResourcesToEnvironment() {
407
+ const links = (args.link || []);
408
+ const resources: any[] = [];
409
+ const customEnv: Record<string, string | Output<string>> = {};
410
+
411
+ links.forEach(link => {
412
+ if (link && typeof link === 'object' && 'resource' in link) {
413
+ // Link is an object with resource and optional envCallback
414
+ resources.push(link.resource);
415
+
416
+ // If there's an envCallback, call it and merge the result
417
+ if (link.envCallback) {
418
+ const callbackResult = link.envCallback(link.resource);
419
+ Object.assign(customEnv, callbackResult);
420
+ }
421
+ } else {
422
+ // Link is just a resource
423
+ resources.push(link);
424
+ }
425
+ });
426
+
427
+ // Apply default environment variables for all resources
428
+ if (!args.config) args.config = {};
429
+ if (!args.config.environment) args.config.environment = {};
430
+
431
+ const resourcesEnvVars = {
432
+ ...applyLinkedResourcesEnv(resources),
433
+ ...customEnv,
434
+ };
435
+
436
+ // TODO: Write resourcesEnvVars to the .env file
437
+ // TODO: Add proper types
438
+ };
439
+
440
+ /**
441
+ * Return the links as an array of resources in the original SST format.
442
+ */
443
+ function getLinks(): any[] {
444
+ return (args.link || []).map(link => {
445
+ if (link && typeof link === 'object' && 'resource' in link) {
446
+ return link.resource;
447
+ }
448
+
449
+ return link;
450
+ });
451
+ }
452
+
453
+ function prepareEnvironmentFile() {
454
+ const envFile = args.config?.environment?.file as string | undefined;
455
+
456
+ if (! envFile) {
457
+ return;
458
+ }
459
+
460
+ const envDir = path.resolve(pluginBuildPath, 'deploy');
461
+ const dst = path.resolve(envDir, '.env');
462
+ const src = path.resolve(absSitePath, envFile);
463
+
464
+ if (fs.existsSync(src)) {
465
+ fs.copyFileSync(src, dst);
466
+ fs.chmodSync(dst, 0o755);
467
+ } else {
468
+ fs.writeFileSync(dst, '');
469
+ }
470
+
471
+ if (! args.config?.environment?.autoInject === false) {
472
+ applyLinkedResourcesToEnvironment();
473
+ }
474
+ }
475
+
476
+ function prepareDeploymentScript() {
477
+ const deployDir = path.resolve(pluginBuildPath, 'deploy');
478
+ const dst = path.resolve(deployDir, '60-deploy.sh');
479
+
480
+ fs.mkdirSync(deployDir, { recursive: true });
481
+
482
+ const script = args.config?.deployment?.script as string | undefined;
483
+ if (script) {
484
+ const src = path.resolve(absSitePath, script);
485
+ if (fs.existsSync(src)) {
486
+ fs.copyFileSync(src, dst);
487
+ fs.chmodSync(dst, 0o755);
488
+ return;
489
+ }
490
+ }
491
+
492
+ fs.writeFileSync(dst, "#!/bin/sh\nexit 0\n");
493
+ fs.chmodSync(dst, 0o755);
494
+ }
495
+ };
496
+ }
497
+
498
+ const __pulumiType = "sst:aws:Laravel";
499
+ // @ts-expect-error
500
+ Laravel.__pulumiType = __pulumiType;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@kirschbaum-development/sst-laravel",
3
+ "version": "0.0.1",
4
+ "description": "An unofficial extension of SST to deploy containerized Laravel applications to AWS Fargate.",
5
+ "main": "laravel-sst.ts",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kirschbaumdevelopment/sst-laravel.git"
9
+ },
10
+ "keywords": [
11
+ "laravel",
12
+ "sst",
13
+ "aws",
14
+ "serverless"
15
+ ],
16
+ "author": {
17
+ "name": "Luis Dalmolin",
18
+ "email": "luis@kirschbaumdevelopment.com"
19
+ },
20
+ "license": "MIT",
21
+ "bugs": {
22
+ "url": "https://github.com/kirschbaum-development/sst-laravel/issues"
23
+ },
24
+ "homepage": "https://github.com/kirschbaum-development/sst-laravel#readme",
25
+ "dependencies": {}
26
+ }
@@ -0,0 +1,154 @@
1
+ import { Email } from "../../../.sst/platform/src/components/aws/email.js";
2
+ import { Mysql } from "../../../.sst/platform/src/components/aws/mysql.js";
3
+ import { Postgres } from "../../../.sst/platform/src/components/aws/postgres.js";
4
+ import { Redis } from "../../../.sst/platform/src/components/aws/redis.js";
5
+ import { Output } from "../../../.sst/platform/node_modules/@pulumi/pulumi/index.js";
6
+ import * as pulumiAws from "../../../.sst/platform/node_modules/@pulumi/aws";
7
+ import { Queue } from "../../../.sst/platform/src/components/aws/queue.js";
8
+ import { Aurora } from "../../../.sst/platform/src/components/aws/aurora.js";
9
+ import { Bucket } from "../../../.sst/platform/src/components/aws/bucket.js";
10
+
11
+ type EnvType = Record<string, string | Output<string>>|Record<string, string | Output<string | undefined> | undefined>;
12
+ type Database = Postgres | Mysql | Aurora | pulumiAws.rds.Instance;
13
+ type LinkSupportedTypes = Database | Email | Queue | Redis | Bucket;
14
+
15
+ export type EnvCallback = (resource: any) => EnvType;
16
+ export type EnvCallbacks = {
17
+ postgres?: EnvCallback;
18
+ mysql?: EnvCallback;
19
+ redis?: EnvCallback;
20
+ email?: EnvCallback;
21
+ queue?: EnvCallback;
22
+ };
23
+
24
+ export function applyLinkedResourcesEnv(links: LinkSupportedTypes[], callbacks?: EnvCallbacks): EnvType {
25
+ let environment: EnvType = {};
26
+
27
+ links.forEach((link: LinkSupportedTypes) => {
28
+ if (link instanceof Postgres) {
29
+ const defaultEnv = applyDatabaseEnv(link);
30
+
31
+ environment = {
32
+ ...environment,
33
+ ...defaultEnv,
34
+ ...(callbacks?.postgres ? callbacks.postgres(link) : {}),
35
+ };
36
+ }
37
+
38
+ if (link instanceof Redis) {
39
+ const defaultEnv = applyRedisEnv(link);
40
+
41
+ environment = {
42
+ ...environment,
43
+ ...defaultEnv,
44
+ ...(callbacks?.redis ? callbacks.redis(link) : {}),
45
+ };
46
+ }
47
+
48
+ if (link instanceof Email) {
49
+ const defaultEnv = applyEmailEnv(link);
50
+
51
+ environment = {
52
+ ...environment,
53
+ ...defaultEnv,
54
+ ...(callbacks?.email ? callbacks.email(link) : {}),
55
+ };
56
+ }
57
+
58
+ if (link instanceof Queue) {
59
+ const defaultEnv = applyQueueEnv(link);
60
+
61
+ environment = {
62
+ ...environment,
63
+ ...defaultEnv,
64
+ ...(callbacks?.queue ? callbacks.queue(link) : {}),
65
+ };
66
+ }
67
+
68
+ if (link instanceof Bucket) {
69
+ const defaultEnv = applyBucketEnv(link);
70
+
71
+ environment = {
72
+ ...environment,
73
+ ...defaultEnv,
74
+ };
75
+ }
76
+ });
77
+
78
+ return environment;
79
+ }
80
+
81
+ function applyDatabaseEnv(database: Database, callbacks?: EnvCallbacks): EnvType {
82
+ let port: number;
83
+ database.port.apply(value => port = value);
84
+
85
+ if (database instanceof Postgres || (database instanceof Aurora && port === 5432)) {
86
+ return applyPostgresEnv(database);
87
+ }
88
+
89
+ if (database instanceof Mysql || (database instanceof Aurora && port === 3306) || database instanceof pulumiAws.rds.Instance) {
90
+ return applyMySqlEnv(database);
91
+ }
92
+
93
+ return {};
94
+ }
95
+
96
+ function applyPostgresEnv(database: Postgres|Aurora): EnvType {
97
+ const port: Output<number> = database.port;
98
+
99
+ return {
100
+ DB_CONNECTION: 'pgsql',
101
+ DB_HOST: database.host,
102
+ DB_DATABASE: database.database,
103
+ DB_USERNAME: database.username,
104
+ DB_PASSWORD: database.password,
105
+ DB_PORT: port.apply(port => port.toString()),
106
+ };
107
+ }
108
+
109
+ function applyMySqlEnv(database: Mysql|Aurora|pulumiAws.rds.Instance): EnvType {
110
+ const port: Output<number> = database.port;
111
+
112
+ return {
113
+ DB_CONNECTION: 'mysql',
114
+ DB_HOST: database instanceof Aurora || database instanceof Mysql ? database.host : database.endpoint,
115
+ DB_DATABASE: database instanceof Aurora || database instanceof Mysql ? database.database : database.dbName,
116
+ DB_USERNAME: database.username,
117
+ DB_PASSWORD: database.password,
118
+ DB_PORT: port.apply(port => port.toString()),
119
+ };
120
+ }
121
+
122
+ export function applyRedisEnv(database: Redis): EnvType {
123
+ // TODO: Check if when encryption at rest is disabled, TLS is not required/throw errors
124
+ return {
125
+ REDIS_HOST: database.host.apply(host => host ? `tls://${host}` : ''),
126
+ REDIS_PORT: database.port.apply(port => port.toString()),
127
+ REDIS_PASSWORD: database.password,
128
+ };
129
+ }
130
+
131
+ // TODO
132
+ export function applyEmailEnv(mail: Email): EnvType {
133
+ return {
134
+ MAIL_MAILER: 'ses',
135
+ // MAIL_FROM_ADDRESS: link.sender,
136
+ };
137
+ }
138
+
139
+ // TODO
140
+ export function applyQueueEnv(queue: Queue): EnvType {
141
+ const queueUrl: Output<string> = queue.url;
142
+
143
+ return {
144
+ SQS_QUEUE: queue.url,
145
+ // MAIL_FROM_ADDRESS: link.sender,
146
+ };
147
+ }
148
+
149
+ export function applyBucketEnv(bucket: Bucket): EnvType {
150
+ return {
151
+ FILESYSTEM_DISK: 's3',
152
+ AWS_BUCKET: bucket.name,
153
+ };
154
+ }
package/sst-env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+
6
+ /// <reference path="../../sst-env.d.ts" />
7
+
8
+ import "sst"
9
+ export {}