├── sf
├── public
│ ├── js
│ │ └── index.html
│ ├── css
│ │ └── index.html
│ ├── img
│ │ └── index.html
│ └── index.php
├── migrations
│ └── .gitignore
├── src
│ ├── Controller
│ │ ├── .gitignore
│ │ ├── MailController.php
│ │ └── SecurityController.php
│ ├── Entity
│ │ └── .gitignore
│ ├── Migrations
│ │ └── .gitignore
│ ├── Repository
│ │ └── .gitignore
│ ├── Twig
│ │ └── JsonDecodeExtension.php
│ ├── EventListener
│ │ └── AuthenticationListener.php
│ ├── Security
│ │ ├── AuthenticationFailureHandler.php
│ │ └── AuthenticationSuccessHandler.php
│ └── Kernel.php
├── translations
│ └── .gitignore
├── .env.master
├── config
│ ├── services_dev.yaml
│ ├── services_test.yaml
│ ├── packages
│ │ ├── test
│ │ │ ├── twig.yaml
│ │ │ ├── routing.yaml
│ │ │ ├── validator.yaml
│ │ │ ├── framework.yaml
│ │ │ ├── web_profiler.yaml
│ │ │ ├── mailer.yaml
│ │ │ ├── security.yaml
│ │ │ ├── dama_doctrine_test_bundle.yaml
│ │ │ ├── monolog.yaml
│ │ │ ├── flysystem.yaml
│ │ │ └── cache.yaml
│ │ ├── dev
│ │ │ ├── routing.yaml
│ │ │ ├── web_profiler.yaml
│ │ │ ├── debug.yaml
│ │ │ ├── flysystem.yaml
│ │ │ ├── easy_log_handler.yaml
│ │ │ └── monolog.yaml
│ │ ├── oat
│ │ │ ├── routing.yaml
│ │ │ ├── deprecations.yaml
│ │ │ ├── doctrine.yaml
│ │ │ └── monolog.yaml
│ │ ├── prod
│ │ │ ├── routing.yaml
│ │ │ ├── deprecations.yaml
│ │ │ ├── doctrine.yaml
│ │ │ └── monolog.yaml
│ │ ├── sensio_framework_extra.yaml
│ │ ├── translation.yaml
│ │ ├── doctrine_migrations.yaml
│ │ ├── routing.yaml
│ │ ├── validator.yaml
│ │ ├── mailer.yaml
│ │ ├── framework.yaml
│ │ ├── cache.yaml
│ │ ├── messenger.yaml
│ │ ├── twig.yaml
│ │ ├── doctrine.yaml
│ │ ├── flysystem.yaml
│ │ └── security.yaml
│ ├── routes
│ │ ├── dev
│ │ │ ├── framework.yaml
│ │ │ └── web_profiler.yaml
│ │ └── annotations.yaml
│ ├── preload.php
│ ├── services_craue.yml
│ ├── routes.yaml
│ ├── bootstrap.php
│ ├── bundles.php
│ └── services.yaml
├── .env.test
├── tests
│ └── bootstrap.php
├── .gitignore
├── .env.dev
├── bin
│ └── console
├── composer.json
├── phpunit.xml.dist
└── symfony.lock
├── .gitignore
├── conf
├── deployer
│ ├── hosts.yml
│ └── deploy.php
├── infra
│ ├── ssh_config
│ ├── msmtp.logrotate
│ ├── virtual-host.conf.tpl
│ ├── php.ini.tpl
│ └── msmtprc.tpl
├── docker
│ ├── files
│ │ └── geolite2
│ │ │ └── GeoLite2-Country.mmdb
│ ├── Dockerfile.cronjob
│ ├── Dockerfile.phpcs
│ ├── docker-compose.cli.local.yml
│ ├── Dockerfile.webinit
│ ├── Dockerfile.phpunit
│ ├── Dockerfile.web.remote
│ └── Dockerfile.web.local
├── k8s
│ ├── service.tpl.yml
│ ├── cronjob.local.tpl.yml
│ ├── deployment.local.tpl.yml
│ ├── cronjob.remote.tpl.yml
│ └── deployment.remote.tpl.yml
├── phpcs
│ └── ruleset.xml
└── env
│ ├── .env.dev.local
│ ├── .env.oat.remote
│ ├── .env.prod.remote
│ └── .env.master.local
├── composer.json
├── docs
├── architecture-images
│ ├── architecture-local.png
│ └── architecture-remote.png
└── architecture.drawio
├── docker-compose.yml
├── composer.lock
└── README.adoc
/sf/public/js/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/migrations/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/public/css/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/public/img/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/src/Controller/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/src/Entity/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/src/Migrations/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/src/Repository/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/translations/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sf/.env.master:
--------------------------------------------------------------------------------
1 | MAILER_PASSWORD=dummy
2 |
--------------------------------------------------------------------------------
/sf/config/services_dev.yaml:
--------------------------------------------------------------------------------
1 | # parameters:
2 |
--------------------------------------------------------------------------------
/sf/config/services_test.yaml:
--------------------------------------------------------------------------------
1 | # parameters:
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | /build/
3 | /cache/
4 | /log/
5 | /var/
6 | /vendor/
7 |
--------------------------------------------------------------------------------
/sf/config/packages/test/twig.yaml:
--------------------------------------------------------------------------------
1 | twig:
2 | strict_variables: true
3 |
--------------------------------------------------------------------------------
/conf/deployer/hosts.yml:
--------------------------------------------------------------------------------
1 | localhost:
2 | local: true
3 |
4 | remote:
5 | local: true
6 |
--------------------------------------------------------------------------------
/sf/config/packages/dev/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: true
4 |
--------------------------------------------------------------------------------
/sf/config/packages/oat/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: null
4 |
--------------------------------------------------------------------------------
/sf/config/packages/prod/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: null
4 |
--------------------------------------------------------------------------------
/sf/config/packages/test/routing.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | router:
3 | strict_requirements: true
4 |
--------------------------------------------------------------------------------
/sf/config/packages/test/validator.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | validation:
3 | not_compromised_password: false
4 |
--------------------------------------------------------------------------------
/sf/config/packages/sensio_framework_extra.yaml:
--------------------------------------------------------------------------------
1 | sensio_framework_extra:
2 | router:
3 | annotations: false
4 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require-dev": {
3 | },
4 | "require": {
5 | "symfony/dotenv": "^4.3"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/conf/infra/ssh_config:
--------------------------------------------------------------------------------
1 | Host github.com
2 | HostName github.com
3 | IdentityFile ~/.ssh/id_rsa
4 | StrictHostKeyChecking no
5 |
--------------------------------------------------------------------------------
/conf/infra/msmtp.logrotate:
--------------------------------------------------------------------------------
1 | /var/log/msmtp/*.log {
2 | rotate 5
3 | daily
4 | compress
5 | missingok
6 | notifempty
7 | }
8 |
--------------------------------------------------------------------------------
/sf/config/routes/dev/framework.yaml:
--------------------------------------------------------------------------------
1 | _errors:
2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
3 | prefix: /_error
4 |
--------------------------------------------------------------------------------
/docs/architecture-images/architecture-local.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericjacolin/symfony-gke/HEAD/docs/architecture-images/architecture-local.png
--------------------------------------------------------------------------------
/conf/docker/files/geolite2/GeoLite2-Country.mmdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericjacolin/symfony-gke/HEAD/conf/docker/files/geolite2/GeoLite2-Country.mmdb
--------------------------------------------------------------------------------
/docs/architecture-images/architecture-remote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ericjacolin/symfony-gke/HEAD/docs/architecture-images/architecture-remote.png
--------------------------------------------------------------------------------
/sf/config/packages/test/framework.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | test: true
3 | secrets: ~
4 | session:
5 | storage_id: session.storage.mock_file
6 |
--------------------------------------------------------------------------------
/sf/config/packages/test/web_profiler.yaml:
--------------------------------------------------------------------------------
1 | web_profiler:
2 | toolbar: false
3 | intercept_redirects: false
4 |
5 | framework:
6 | profiler: { collect: false }
7 |
--------------------------------------------------------------------------------
/sf/config/packages/dev/web_profiler.yaml:
--------------------------------------------------------------------------------
1 | web_profiler:
2 | toolbar: true
3 | intercept_redirects: false
4 |
5 | framework:
6 | profiler: { only_exceptions: false }
7 |
--------------------------------------------------------------------------------
/sf/config/packages/test/mailer.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | mailer:
3 | transports:
4 | #main: '%env(MAILER_DSN)%'
5 | realtime_mailer: 'null://default'
6 |
--------------------------------------------------------------------------------
/sf/config/packages/test/security.yaml:
--------------------------------------------------------------------------------
1 | security:
2 | encoders:
3 | App\Entity\User:
4 | # Faster login for tests
5 | cost: 4
6 | algorithm: auto
7 |
--------------------------------------------------------------------------------
/sf/config/routes/annotations.yaml:
--------------------------------------------------------------------------------
1 | controllers:
2 | resource: ../../src/Controller/
3 | type: annotation
4 |
5 | kernel:
6 | resource: ../../src/Kernel.php
7 | type: annotation
8 |
--------------------------------------------------------------------------------
/sf/config/packages/test/dama_doctrine_test_bundle.yaml:
--------------------------------------------------------------------------------
1 | dama_doctrine_test:
2 | enable_static_connection: true
3 | enable_static_meta_data_cache: true
4 | enable_static_query_cache: true
5 |
--------------------------------------------------------------------------------
/sf/config/packages/translation.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | default_locale: en
3 | translator:
4 | default_path: '%kernel.project_dir%/translations'
5 | fallbacks:
6 | - en
7 |
--------------------------------------------------------------------------------
/sf/.env.test:
--------------------------------------------------------------------------------
1 | API_KEY=dummy.test
2 | APP_SECRET=dummy.test
3 | DB_PASSWORD=dbpwd_test
4 | MAILER_PASSWORD=dummy
5 |
6 | GCS_PRIVATE_BUCKET=app-test.myproject.info
7 | GCS_PUBLIC_BUCKET=cdn-test.myproject.info
8 |
--------------------------------------------------------------------------------
/sf/config/preload.php:
--------------------------------------------------------------------------------
1 | bootEnv(dirname(__DIR__).'/.env');
11 | }
12 |
--------------------------------------------------------------------------------
/sf/.gitignore:
--------------------------------------------------------------------------------
1 | ###> symfony/framework-bundle ###
2 | /.env
3 | /.env.local
4 | /.env.*.local
5 | /bin/*
6 | !/bin/console
7 | /config/secrets/*/*.decrypt.private.php
8 | /public/bundles/
9 | /var/
10 | /vendor/
11 | ###< symfony/framework-bundle ###
12 |
13 | ###> symfony/phpunit-bridge ###
14 | /.phpunit
15 | /.phpunit.result.cache
16 | /phpunit.xml
17 | ###< symfony/phpunit-bridge ###
18 |
--------------------------------------------------------------------------------
/sf/config/services_craue.yml:
--------------------------------------------------------------------------------
1 | # Separate file to avoid _defaults.bind error when calling a parent class as _defaults apply at file level
2 | services:
3 | # Craue multi-page form
4 | App\Form\SomeEntity\SomeFlow:
5 | autowire: true
6 | autoconfigure: false
7 | public: true
8 | parent: craue.form.flow
9 | tags:
10 | - { name: form.type }
11 |
--------------------------------------------------------------------------------
/sf/config/packages/test/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: fingers_crossed
5 | action_level: error
6 | handler: nested
7 | excluded_http_codes: [404, 405]
8 | channels: ["!event"]
9 | nested:
10 | type: stream
11 | path: "%kernel.logs_dir%/%kernel.environment%.log"
12 | level: debug
13 |
--------------------------------------------------------------------------------
/sf/config/packages/dev/flysystem.yaml:
--------------------------------------------------------------------------------
1 | flysystem:
2 | storages:
3 | storage.private.local:
4 | adapter: 'local'
5 | options:
6 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PRIVATE_BUCKET)%'
7 | storage.public.local:
8 | adapter: 'local'
9 | options:
10 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PUBLIC_BUCKET)%'
11 |
--------------------------------------------------------------------------------
/sf/config/packages/test/flysystem.yaml:
--------------------------------------------------------------------------------
1 | flysystem:
2 | storages:
3 | storage.private.local:
4 | adapter: 'local'
5 | options:
6 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PRIVATE_BUCKET)%'
7 | storage.public.local:
8 | adapter: 'local'
9 | options:
10 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PUBLIC_BUCKET)%'
11 |
--------------------------------------------------------------------------------
/conf/infra/virtual-host.conf.tpl:
--------------------------------------------------------------------------------
1 |
2 | DocumentRoot /var/www/sf/public
3 | ServerName {{ .Env.ROUTER_REQUEST_CONTEXT_HOST }}
4 | ServerAdmin info@myproject.info
5 |
6 | AllowOverride AuthConfig
7 | Require all granted
8 | RewriteEngine On
9 | RewriteCond %{REQUEST_FILENAME} !-f
10 | RewriteRule ^(.*)$ index.php [QSA,L]
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sf/config/routes.yaml:
--------------------------------------------------------------------------------
1 | #index:
2 | # path: /
3 | # controller: App\Controller\DefaultController::index
4 |
5 | login:
6 | path: /login
7 | controller: App\Controller\DefaultController::homeAction
8 |
9 | logout:
10 | path: /logout
11 |
12 |
13 | # Static content pages
14 |
15 | #static_tandc:
16 | # path: /tandc
17 | # defaults:
18 | # _controller: FrameworkBundle:Template:template
19 | # template: static/tandc.html.twig
20 |
--------------------------------------------------------------------------------
/sf/src/Twig/JsonDecodeExtension.php:
--------------------------------------------------------------------------------
1 | bootEnv(dirname(__DIR__).'/.env');
11 |
12 | if ($_SERVER['APP_DEBUG']) {
13 | umask(0000);
14 |
15 | Debug::enable();
16 | }
17 |
18 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
19 | $request = Request::createFromGlobals();
20 | $response = $kernel->handle($request);
21 | $response->send();
22 | $kernel->terminate($request, $response);
23 |
--------------------------------------------------------------------------------
/conf/infra/php.ini.tpl:
--------------------------------------------------------------------------------
1 | ; Errors
2 | display_errors = {{ .Env.PHP_DISPLAY_ERRORS }}
3 | display_startup_errors = On
4 | error_reporting = {{ .Env.PHP_ERROR_REPORTING }}
5 | log_errors = On
6 | error_log = /var/www/var/log/php_error.log
7 |
8 | ; Session
9 | session.gc_maxlifetime = 7200
10 |
11 | ; File uploads
12 | upload_max_filesize = 16M
13 | post_max_size = 16M
14 |
15 | ; mail function
16 | sendmail_path = "/usr/bin/msmtp -t -v"
17 |
18 | ; opcache
19 | opcache.enable={{ .Env.OPCACHE_ENABLE }}
20 | opcache.preload_user=www-data
21 | opcache.memory_consumption=256
22 | opcache.max_accelerated_files=20000
23 | opcache.preload={{ .Env.OPCACHE_PRELOAD }}
24 |
--------------------------------------------------------------------------------
/sf/config/packages/oat/doctrine.yaml:
--------------------------------------------------------------------------------
1 | doctrine:
2 | orm:
3 | auto_generate_proxy_classes: false
4 | metadata_cache_driver:
5 | type: pool
6 | pool: doctrine.system_cache_pool
7 | query_cache_driver:
8 | type: pool
9 | pool: doctrine.system_cache_pool
10 | result_cache_driver:
11 | type: pool
12 | pool: doctrine.result_cache_pool
13 |
14 | framework:
15 | cache:
16 | pools:
17 | doctrine.result_cache_pool:
18 | adapter: cache.app
19 | doctrine.system_cache_pool:
20 | adapter: cache.system
21 |
--------------------------------------------------------------------------------
/sf/config/packages/prod/doctrine.yaml:
--------------------------------------------------------------------------------
1 | doctrine:
2 | orm:
3 | auto_generate_proxy_classes: false
4 | metadata_cache_driver:
5 | type: pool
6 | pool: doctrine.system_cache_pool
7 | query_cache_driver:
8 | type: pool
9 | pool: doctrine.system_cache_pool
10 | result_cache_driver:
11 | type: pool
12 | pool: doctrine.result_cache_pool
13 |
14 | framework:
15 | cache:
16 | pools:
17 | doctrine.result_cache_pool:
18 | adapter: cache.app
19 | doctrine.system_cache_pool:
20 | adapter: cache.system
21 |
--------------------------------------------------------------------------------
/conf/docker/Dockerfile.phpcs:
--------------------------------------------------------------------------------
1 | FROM php:7.4-cli
2 |
3 | LABEL maintainer=
4 |
5 | ENV PHPCS_VERSION=3.5.8
6 |
7 | RUN apt-get update \
8 | && apt-get install -y \
9 | curl \
10 | git \
11 | # zip
12 | && apt-get install -y \
13 | libzip-dev \
14 | zip \
15 | && docker-php-ext-install zip \
16 | # Clean up
17 | && apt-get clean && \
18 | rm -rf /var/lib/lists/*
19 |
20 | RUN curl -L https://github.com/squizlabs/PHP_CodeSniffer/releases/download/$PHPCS_VERSION/phpcs.phar > /usr/local/bin/phpcs \
21 | && chmod +x /usr/local/bin/phpcs
22 |
23 | WORKDIR "/project"
24 |
25 | ENTRYPOINT ["phpcs"]
26 | CMD ["--version"]
27 |
--------------------------------------------------------------------------------
/sf/config/packages/dev/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: stream
5 | path: "%kernel.logs_dir%/%kernel.environment%.log"
6 | level: debug
7 | channels: ["!event"]
8 | # uncomment to get logging in your browser
9 | # you may have to allow bigger header sizes in your Web server configuration
10 | #firephp:
11 | # type: firephp
12 | # level: info
13 | #chromephp:
14 | # type: chromephp
15 | # level: info
16 | console:
17 | type: console
18 | process_psr_3_messages: false
19 | channels: ["!event", "!doctrine", "!console"]
20 |
--------------------------------------------------------------------------------
/conf/phpcs/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Eric's custom coding standard.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/sf/config/packages/framework.yaml:
--------------------------------------------------------------------------------
1 | # see https://symfony.com/doc/current/reference/configuration/framework.html
2 | framework:
3 | secret: '%env(APP_SECRET)%'
4 | default_locale: '%locale%'
5 | csrf_protection: false
6 | http_method_override: true
7 |
8 | # Enables session support. Note that the session will ONLY be started if you read or write from it.
9 | # Remove or comment this section to explicitly disable session support.
10 | session:
11 | handler_id: null
12 | cookie_secure: auto
13 | cookie_samesite: lax
14 |
15 | #esi: true
16 | #fragments: true
17 | php_errors:
18 | log: true
19 |
20 | trusted_proxies: '%env(TRUSTED_PROXIES)%'
21 | trusted_headers: ['x-forwarded-for', 'x-forwarded-host']
22 |
--------------------------------------------------------------------------------
/sf/config/packages/cache.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | cache:
3 | # Unique name of your app: used to compute stable namespaces for cache keys.
4 | #prefix_seed: your_vendor_name/app_name
5 |
6 | # The "app" cache stores to the filesystem by default.
7 | # The data in this cache should persist between deploys.
8 | # Other options include:
9 |
10 | # Redis
11 | #app: cache.adapter.redis
12 | #default_redis_provider: redis://localhost
13 |
14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
15 | app: cache.adapter.apcu
16 |
17 | # Namespaced pools use the above "app" backend by default
18 | #pools:
19 | #my.dedicated.cache: null
20 |
--------------------------------------------------------------------------------
/conf/infra/msmtprc.tpl:
--------------------------------------------------------------------------------
1 | defaults
2 |
3 | logfile /var/log/msmtp/msmtp.log
4 |
5 | # OVH
6 | account ovh
7 | host ssl0.ovh.net
8 | auth on
9 | port 465
10 | from info@myproject.info
11 | user eric@myproject.info
12 | passwordeval "echo $MAILER_PASSWORD"
13 | auth on
14 | tls on
15 | tls_trust_file /etc/ssl/certs/ca-certificates.crt
16 | # https://wiki.archlinux.org/index.php/msmtp#Server_sent_empty_reply
17 | tls_starttls off
18 |
19 | # mailcatcher (local)
20 | account mailcatcher
21 | host 10.0.2.2
22 | port 1025
23 | from dummy@myproject.info
24 | auth off
25 | tls off
26 |
27 | # Set a default account
28 | account default : {{ .Env.SMTP_ACCOUNT }}
29 |
--------------------------------------------------------------------------------
/sf/config/packages/test/cache.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | cache:
3 | # Unique name of your app: used to compute stable namespaces for cache keys.
4 | #prefix_seed: your_vendor_name/app_name
5 |
6 | # The "app" cache stores to the filesystem by default.
7 | #app: cache.adapter.filesystem
8 | # The data in this cache should persist between deploys.
9 | # Other options include:
10 |
11 | # Redis
12 | #app: cache.adapter.redis
13 | #default_redis_provider: redis://localhost
14 |
15 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
16 | #app: cache.adapter.apcu
17 |
18 | # Namespaced pools use the above "app" backend by default
19 | #pools:
20 | #my.dedicated.cache: null
21 |
--------------------------------------------------------------------------------
/sf/config/packages/messenger.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | messenger:
3 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
4 | failure_transport: failed
5 |
6 | transports:
7 | # https://symfony.com/doc/current/messenger.html#transport-configuration
8 | failed: 'doctrine://default?queue_name=failed'
9 | # async: '%env(MESSENGER_TRANSPORT_DSN)%'
10 | # sync: 'sync://'
11 | batch_mailer: 'doctrine://default?queue_name=batch_mailer'
12 | realtime_mailer: 'doctrine://default?queue_name=realtime_mailer'
13 |
14 | routing:
15 | # Route your messages to the transports
16 | 'Symfony\Component\Mailer\Messenger\SendEmailMessage': realtime_mailer
17 | # 'App\Message\YourMessage': async
18 |
--------------------------------------------------------------------------------
/sf/config/packages/twig.yaml:
--------------------------------------------------------------------------------
1 | twig:
2 | default_path: '%kernel.project_dir%/templates'
3 | debug: '%kernel.debug%'
4 | strict_variables: '%kernel.debug%'
5 |
6 | # Twig helper services
7 | globals:
8 | # Services
9 | some_utils: '@App\Utils\SomeUtils'
10 |
11 | # Parameters
12 | cdn_url: "%cdn_url%"
13 | cdn_url_from_cluster: "%cdn_url_from_cluster%"
14 | site_url: "%router.request_context.scheme%://%router.request_context.host%"
15 |
16 | # Asset cache busting
17 | asset_v: "%asset_v%"
18 |
19 | # Global form theming
20 | form_themes:
21 | - 'form/form_fields.html.twig'
22 |
23 | # Deprecation - see https://github.com/symfony/symfony/blob/master/UPGRADE-4.4.md
24 | # exception_controller: null
25 |
26 | paths:
27 | '%kernel.project_dir%/templates/email': email
28 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 |
5 | phpunit:
6 | build:
7 | context: .
8 | dockerfile: conf/docker/Dockerfile.phpunit
9 | restart: "no"
10 | volumes:
11 | - "/projects/myproject/sf:/var/www/sf"
12 | - "/storage-buckets:/var/www/local-storage"
13 | network_mode: "host"
14 |
15 | composer:
16 | build:
17 | context: .
18 | dockerfile: conf/docker/Dockerfile.composer
19 | restart: "no"
20 | volumes:
21 | - "/projects/myproject/sf:/var/www/sf"
22 | network_mode: "host"
23 |
24 | phpcs:
25 | build:
26 | context: .
27 | dockerfile: conf/docker/Dockerfile.phpcs
28 | restart: "no"
29 | volumes:
30 | - "/projects/myproject:/project"
31 | network_mode: "host"
32 |
--------------------------------------------------------------------------------
/conf/docker/docker-compose.cli.local.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 |
5 | phpunit:
6 | build:
7 | context: .
8 | dockerfile: conf/docker/Dockerfile.phpunit
9 | restart: "no"
10 | volumes:
11 | - "/projects/myproject/sf:/var/www/sf"
12 | - "/storage/myproject:/var/www/local-storage"
13 | network_mode: "host"
14 |
15 | composer:
16 | build:
17 | context: .
18 | dockerfile: conf/docker/Dockerfile.composer
19 | restart: "no"
20 | volumes:
21 | - "/projects/myproject/sf:/var/www/sf"
22 | network_mode: "host"
23 |
24 | phpcs:
25 | build:
26 | context: .
27 | dockerfile: conf/docker/Dockerfile.phpcs
28 | restart: "no"
29 | volumes:
30 | - "/projects/myproject:/project"
31 | network_mode: "host"
32 |
--------------------------------------------------------------------------------
/sf/config/packages/oat/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: fingers_crossed
5 | action_level: error
6 | handler: grouped
7 | excluded_http_codes: [404, 405]
8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks
9 | grouped:
10 | type: group
11 | members: [streamed, deduplicated]
12 | streamed:
13 | type: rotating_file
14 | path: "%kernel.logs_dir%/%kernel.environment%.log"
15 | level: debug
16 | max_files: 10
17 | deduplicated:
18 | type: deduplication
19 | handler: symfony_mailer
20 | symfony_mailer:
21 | type: symfony_mailer
22 | from_email: "%email_from%"
23 | to_email: "%email_from%"
24 | subject: 'An Error Occurred! %%message%%'
25 | level: debug
26 | formatter: monolog.formatter.html
27 | content_type: text/html
28 |
--------------------------------------------------------------------------------
/sf/config/packages/prod/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | handlers:
3 | main:
4 | type: fingers_crossed
5 | action_level: error
6 | handler: grouped
7 | excluded_http_codes: [404, 405]
8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks
9 | grouped:
10 | type: group
11 | members: [streamed, deduplicated]
12 | streamed:
13 | type: rotating_file
14 | path: "%kernel.logs_dir%/%kernel.environment%.log"
15 | level: debug
16 | max_files: 10
17 | deduplicated:
18 | type: deduplication
19 | handler: symfony_mailer
20 | symfony_mailer:
21 | type: symfony_mailer
22 | from_email: "%email_from%"
23 | to_email: "%email_from%"
24 | subject: 'An Error Occurred! %%message%%'
25 | level: debug
26 | formatter: monolog.formatter.html
27 | content_type: text/html
28 |
--------------------------------------------------------------------------------
/sf/config/bootstrap.php:
--------------------------------------------------------------------------------
1 | =1.2)
13 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
14 | (new Dotenv(false))->populate($env);
15 | } else {
16 | // load all the .env files
17 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
18 | }
19 |
20 | $_SERVER += $_ENV;
21 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
22 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
23 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
24 |
--------------------------------------------------------------------------------
/sf/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
6 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
7 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
8 | Craue\FormFlowBundle\CraueFormFlowBundle::class => ['all' => true],
9 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
10 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
11 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
12 | League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
13 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
14 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
15 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
16 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
17 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
18 | ];
19 |
--------------------------------------------------------------------------------
/conf/env/.env.dev.local:
--------------------------------------------------------------------------------
1 | PROJECT=
2 |
3 | ### Symfony environment
4 | APP_ENV=dev
5 | APP_DEBUG=1
6 | #APP_SECRET=SYMFONY_DECRYPTION_SECRET
7 | #API_KEY=K8S_SECRET
8 |
9 | ### Bind mounts (local only)
10 | HOST_SF_DIR=/hosthome/myproject/sf
11 | HOST_DATA_LOCAL_STORAGE=/hosthome/storage-buckets
12 |
13 | ### K8s resources
14 | WEB_POD_URL_INTERNAL=http://myproject-dev
15 |
16 | ### Application host names and protocol
17 | ROUTER_REQUEST_CONTEXT_HOST=dev.myproject
18 | ROUTER_REQUEST_CONTEXT_SCHEME=http
19 | CDN_URL=http://cdn-dev.myproject
20 | CDN_URL_FROM_CLUSTER=http://cdn-dev.myproject
21 | TRUSTED_PROXIES=172.0.0.0/8
22 | OPCACHE_ENABLE=0
23 | OPCACHE_PRELOAD=
24 |
25 | ### Docker build arguments
26 | PHP_DISPLAY_ERRORS=On
27 | PHP_ERROR_REPORTING=E_ALL
28 |
29 | ### Storage abstraction
30 | STORAGE_ADAPTER=local
31 | ### GCS adapter
32 | GCS_PRIVATE_BUCKET=app-dev.myproject.info
33 | GCS_PUBLIC_BUCKET=cdn-dev.myproject.info
34 |
35 | ### MySQL
36 | DB_HOST=10.0.2.2
37 | DB_PORT=3306
38 | DB_NAME=myproject_dev
39 | DB_USER=myproject_dev
40 | #DB_PASSWORD=SYMFONY_DECRYPTION_SECRET
41 |
42 | ### SMTP
43 | SMTP_ACCOUNT=mailcatcher
44 | MAILER_TRANSPORT=smtp
45 | MAILER_HOST=ssl0.ovh.net
46 | MAILER_PORT=465
47 | MAILER_ENCRYPTION=ssl
48 | MAILER_USER=info@myproject.info
49 | #MAILER_PASSWORD=SYMFONY_DECRYPTION_SECRET
50 | EMAIL_FROM=dummy@myproject.info
51 |
--------------------------------------------------------------------------------
/conf/env/.env.oat.remote:
--------------------------------------------------------------------------------
1 | PROJECT=myproject
2 |
3 | ### Symfony environment
4 | APP_ENV=oat
5 | APP_DEBUG=0
6 | #APP_SECRET=SYMFONY_DECRYPTION_SECRET
7 | #API_KEY=K8S_SECRET
8 |
9 | ### Bind mounts
10 | HOST_SF_DIR=
11 | HOST_DATA_LOCAL_STORAGE=
12 |
13 | ### K8s resources
14 | WEB_POD_URL_INTERNAL=http://myproject-oat
15 |
16 | ### Application host names and protocol
17 | ROUTER_REQUEST_CONTEXT_HOST=test.myproject.info
18 | ROUTER_REQUEST_CONTEXT_SCHEME=https
19 | CDN_URL=https://cdn-test.myproject.info
20 | CDN_URL_FROM_CLUSTER=https://cdn-test.myproject.info
21 | TRUSTED_PROXIES=172.0.0.0/8
22 | OPCACHE_ENABLE=0
23 | OPCACHE_PRELOAD=/var/www/cache/oat/App_KernelOatContainer.preload.php
24 |
25 | ### Docker build arguments
26 | PHP_DISPLAY_ERRORS=Off
27 | PHP_ERROR_REPORTING=E_ALL
28 |
29 | ### Storage abstraction
30 | STORAGE_ADAPTER=gcloud
31 | ### GCS adapter
32 | GCS_PRIVATE_BUCKET=app-test.myproject.info
33 | GCS_PUBLIC_BUCKET=cdn-test.myproject.info
34 |
35 | ### MySQL
36 | DB_HOST=10.1.2.3
37 | DB_PORT=3306
38 | DB_NAME=myproject_dev
39 | DB_USER=myproject_dev
40 | #DB_PASSWORD=SYMFONY_DECRYPTION_SECRET
41 |
42 | ### SMTP
43 | SMTP_ACCOUNT=ovh
44 | MAILER_TRANSPORT=smtp
45 | MAILER_HOST=ssl0.ovh.net
46 | MAILER_PORT=465
47 | MAILER_ENCRYPTION=ssl
48 | MAILER_USER=info@myproject.info
49 | #MAILER_PASSWORD=SYMFONY_DECRYPTION_SECRET
50 | EMAIL_FROM=info@myproject.info
51 |
--------------------------------------------------------------------------------
/conf/env/.env.prod.remote:
--------------------------------------------------------------------------------
1 | PROJECT=myproject
2 |
3 | ### Symfony environment
4 | APP_ENV=prod
5 | APP_DEBUG=0
6 | #APP_SECRET=SYMFONY_DECRYPTION_SECRET
7 | #API_KEY=K8S_SECRET
8 |
9 | ### Bind mounts
10 | HOST_SF_DIR=
11 | HOST_DATA_LOCAL_STORAGE=
12 |
13 | ### K8s resources
14 | WEB_POD_URL_INTERNAL=http://myproject-prod
15 |
16 | ### Application host names and protocol
17 | ROUTER_REQUEST_CONTEXT_HOST=www.myproject.info
18 | ROUTER_REQUEST_CONTEXT_SCHEME=https
19 | CDN_URL=https://cdn.myproject.info
20 | CDN_URL_FROM_CLUSTER=https://cdn.myproject.info
21 | TRUSTED_PROXIES=172.0.0.0/8
22 | OPCACHE_ENABLE=0
23 | OPCACHE_PRELOAD=/var/www/cache/prod/App_KernelProdContainer.preload.php
24 |
25 | ### Docker build arguments
26 | PHP_DISPLAY_ERRORS=Off
27 | PHP_ERROR_REPORTING=E_ERROR|E_WARNING|E_PARSE
28 |
29 | ### Storage abstraction
30 | STORAGE_ADAPTER=gcloud
31 | ### GCS adapter
32 | GCS_PRIVATE_BUCKET=app.myproject.info
33 | GCS_PUBLIC_BUCKET=cdn.myproject.info
34 |
35 | ### MySQL
36 | DB_HOST=10.1.2.3
37 | DB_PORT=3306
38 | DB_NAME=myproject_master
39 | DB_USER=myproject_master
40 | #DB_PASSWORD=SYMFONY_DECRYPTION_SECRET
41 |
42 | ### SMTP
43 | SMTP_ACCOUNT=ovh
44 | MAILER_TRANSPORT=smtp
45 | MAILER_HOST=ssl0.ovh.net
46 | MAILER_PORT=465
47 | MAILER_ENCRYPTION=ssl
48 | MAILER_USER=info@myproject.info
49 | #MAILER_PASSWORD=SYMFONY_DECRYPTION_SECRET
50 | EMAIL_FROM=info@myproject.info
51 |
--------------------------------------------------------------------------------
/conf/env/.env.master.local:
--------------------------------------------------------------------------------
1 | PROJECT=myproject
2 |
3 | ### Symfony environment
4 | APP_ENV=master
5 | APP_DEBUG=1
6 | #APP_SECRET=SYMFONY_DECRYPTION_SECRET
7 | #API_KEY=K8S_SECRET
8 |
9 | ### Bind mounts (local only)
10 | HOST_SF_DIR=/hosthome/myproject/sf
11 | HOST_DATA_LOCAL_STORAGE=/hosthome/storage-buckets
12 |
13 | ### K8s resources
14 | WEB_POD_URL_INTERNAL=http://myproject-master
15 |
16 | ### Application host names and protocol
17 | ROUTER_REQUEST_CONTEXT_HOST=master.myproject
18 | ROUTER_REQUEST_CONTEXT_SCHEME=http
19 | CDN_URL=http://cdn.myproject:10080
20 | CDN_URL_FROM_CLUSTER=http://10.0.2.2:10082
21 | TRUSTED_PROXIES=172.0.0.0/8
22 | OPCACHE_ENABLE=1
23 | OPCACHE_PRELOAD=/var/www/cache/master/App_KernelMasterDebugContainer.preload.php
24 |
25 | ### Docker build arguments
26 | PHP_DISPLAY_ERRORS=Off
27 | PHP_ERROR_REPORTING=E_ALL
28 |
29 | ### Storage abstraction
30 | STORAGE_ADAPTER=local
31 | ### GCS adapter
32 | GCS_PRIVATE_BUCKET=app-master.myproject.info
33 | GCS_PUBLIC_BUCKET=cdn-master.myproject.info
34 |
35 | ### MySQL
36 | DB_HOST=10.0.2.2
37 | DB_PORT=3306
38 | DB_NAME=myproject_master
39 | DB_USER=myproject_master
40 | #DB_PASSWORD=SYMFONY_DECRYPTION_SECRET
41 |
42 | ### SMTP
43 | SMTP_ACCOUNT=ovh
44 | MAILER_TRANSPORT=smtp
45 | MAILER_HOST=ssl0.ovh.net
46 | MAILER_PORT=465
47 | MAILER_ENCRYPTION=ssl
48 | MAILER_USER=info@myproject.info
49 | #MAILER_PASSWORD=SYMFONY_DECRYPTION_SECRET
50 | EMAIL_FROM=dummy@myproject.info
51 |
--------------------------------------------------------------------------------
/sf/config/packages/doctrine.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | # Adds a fallback DATABASE_URL if the env var is not set.
3 | # This allows you to run cache:warmup even if your
4 | # environment variables are not available yet.
5 | # You should not need to change this value.
6 | env(DATABASE_URL): ''
7 |
8 | doctrine:
9 | dbal:
10 | # configure these for your database server
11 | driver: 'pdo_mysql'
12 | server_version: '5.7'
13 | charset: utf8mb4
14 | default_table_options:
15 | charset: utf8mb4
16 | collate: utf8mb4_unicode_ci
17 |
18 | dbname: '%env(DB_NAME)%'
19 | user: '%env(DB_USER)%'
20 | password: '%env(DB_PASSWORD)%'
21 | host: '%env(DB_HOST)%'
22 | port: '%env(DB_PORT)%'
23 | # the following needed as Doctrine doesn't natively support ENUM
24 | mapping_types:
25 | enum: string
26 | # UUID custom type
27 | types:
28 | uuid: Ramsey\Uuid\Doctrine\UuidType
29 |
30 | orm:
31 | auto_generate_proxy_classes: true
32 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
33 | auto_mapping: true
34 | mappings:
35 | App:
36 | is_bundle: false
37 | type: annotation
38 | dir: '%kernel.project_dir%/src/Entity'
39 | prefix: 'App\Entity'
40 | alias: App
41 |
--------------------------------------------------------------------------------
/sf/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getParameterOption(['--env', '-e'], null, true)) {
24 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
25 | }
26 |
27 | if ($input->hasParameterOption('--no-debug', true)) {
28 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
29 | }
30 |
31 | (new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
32 |
33 | if ($_SERVER['APP_DEBUG']) {
34 | umask(0000);
35 |
36 | if (class_exists(Debug::class)) {
37 | Debug::enable();
38 | }
39 | }
40 |
41 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
42 | $application = new Application($kernel);
43 | $application->run($input);
44 |
--------------------------------------------------------------------------------
/sf/src/EventListener/AuthenticationListener.php:
--------------------------------------------------------------------------------
1 | getRequest();
30 | $user = $event->getAuthenticationToken()->getUser();
31 | // Initiate session
32 | // your code here
33 | }
34 |
35 | /**
36 | * onAuthenticationFailure
37 | *
38 | * @param AuthenticationFailureEvent $event
39 | */
40 | public function onAuthenticationFailure(
41 | AuthenticationFailureEvent $event
42 | ) {
43 | $token = $event->getAuthenticationToken();
44 | $username = $token->getUsername();
45 |
46 | // your code here
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/sf/config/packages/flysystem.yaml:
--------------------------------------------------------------------------------
1 | flysystem:
2 | storages:
3 | storage.private.local:
4 | adapter: 'local'
5 | options:
6 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PRIVATE_BUCKET)%'
7 | storage.public.local:
8 | adapter: 'local'
9 | options:
10 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PUBLIC_BUCKET)%'
11 | storage.private.gcloud:
12 | adapter: 'gcloud'
13 | options:
14 | client: 'Google\Cloud\Storage\StorageClient' # The service ID of the Google\Cloud\Storage\StorageClient instance
15 | bucket: '%env(GCS_PRIVATE_BUCKET)%'
16 | prefix: ''
17 | api_url: 'https://storage.googleapis.com'
18 | storage.public.gcloud:
19 | adapter: 'gcloud'
20 | options:
21 | client: 'Google\Cloud\Storage\StorageClient' # The service ID of the Google\Cloud\Storage\StorageClient instance
22 | bucket: '%env(GCS_PUBLIC_BUCKET)%'
23 | prefix: ''
24 | api_url: 'https://storage.googleapis.com'
25 | # Aliases based on environment variable
26 | storage.private:
27 | adapter: 'lazy'
28 | options:
29 | source: 'storage.private.%env(STORAGE_ADAPTER)%'
30 | storage.public:
31 | adapter: 'lazy'
32 | options:
33 | source: 'storage.public.%env(STORAGE_ADAPTER)%'
34 |
--------------------------------------------------------------------------------
/sf/src/Security/AuthenticationFailureHandler.php:
--------------------------------------------------------------------------------
1 | router = $router;
26 | }
27 |
28 | /**
29 | * This is called when an interactive authentication attempt succeeds. This
30 | * is called by authentication listeners inheriting from AbstractAuthenticationListener.
31 | * @param Request $request
32 | * @param TokenInterface $token
33 | * @return Response The response to return
34 | */
35 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
36 | {
37 | // Flash message
38 | $request->getSession()->getFlashBag()->add('error', 'Incorrect email or password');
39 | // The failure redirection path
40 | $path = !is_null($request->get('_failure_path')) ? $request->get('_failure_path') : '/';
41 | return new RedirectResponse($path);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/sf/src/Controller/MailController.php:
--------------------------------------------------------------------------------
1 | vu = $vu;
29 | }
30 |
31 | /**
32 | * Send spooled emails (real-time)
33 | *
34 | * @Route("/api/email_send_realtime/{api_key}", name="email_send_realtime",
35 | * requirements={"api_key": "[\w]{12}"})
36 | */
37 | public function emailSendRealtimeAction(KernelInterface $kernel, string $api_key)
38 | {
39 | // Validate API key
40 | $this->vu->validateApiKey($api_key);
41 |
42 | $application = new Application($kernel);
43 | $application->setAutoExit(false);
44 |
45 | $input = new ArrayInput([
46 | 'command' => 'messenger:consume',
47 | 'receivers' => ['realtime_mailer'],
48 | '--limit' => 20,
49 | '--time-limit' => 100,
50 | ]);
51 | $output = new NullOutput();
52 | $application->run($input, $output);
53 | return new Response();
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/sf/src/Controller/SecurityController.php:
--------------------------------------------------------------------------------
1 | auth Authentication Utilities */
24 | private $auth;
25 |
26 | public function __construct(
27 | AuthenticationUtils $auth,
28 | RouterInterface $router
29 | ) {
30 | $this->auth = $auth;
31 | $this->router = $router;
32 | }
33 |
34 |
35 | /**
36 | * Display login form
37 | */
38 | public function loginEmbeddedShowAction(
39 | Request $request,
40 | $site_domain,
41 | $token = null
42 | ) {
43 | // Evaluate the Target paths - your code here
44 | // $target_path = ...
45 | // $failure_path = ...
46 |
47 | $form = $this->createForm(LoginForm::class, [
48 | 'site_domain' => $site_domain,
49 | // Target paths
50 | '_target_path' => $target_path,
51 | '_failure_path' => $failure_path,
52 | ]);
53 |
54 | return $this->render('security/login.html.twig', [
55 | 'form' => $form->createView(),
56 | ]);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/sf/src/Security/AuthenticationSuccessHandler.php:
--------------------------------------------------------------------------------
1 | router = $router;
33 | $this->session = $session;
34 | }
35 |
36 | /**
37 | * This is called when an interactive authentication attempt succeeds. This
38 | * is called by authentication listeners inheriting from AbstractAuthenticationListener.
39 | * @param Request $request
40 | * @param TokenInterface $token
41 | * @return Response The response to return
42 | */
43 | public function onAuthenticationSuccess(Request $request, TokenInterface $token)
44 | {
45 | // Session variables are set by the AuthenticationListener
46 |
47 | // The failure redirection path
48 | $path = !is_null($request->get('_target_path')) ? $request->get('_target_path') : '/';
49 | return new RedirectResponse($path);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/conf/k8s/cronjob.local.tpl.yml:
--------------------------------------------------------------------------------
1 | apiVersion: batch/v1beta1
2 | kind: CronJob
3 | metadata:
4 | name: {{ PROJECT }}-{{ }}-mailer-realtime
5 | labels:
6 | cron-job: {{ PROJECT }}-{{ APP_ENV }}-mailer-realtime
7 | spec:
8 | schedule: "*/2 * * * *"
9 | successfulJobsHistoryLimit: 1
10 | failedJobsHistoryLimit: 1
11 | concurrencyPolicy: Forbid
12 | startingDeadlineSeconds: 60
13 | jobTemplate:
14 | spec:
15 | template:
16 | spec:
17 | restartPolicy: OnFailure
18 | containers:
19 | - name: k8s-cronjob
20 | image: k8s-cronjob:current
21 | env:
22 | - name: API_KEY
23 | valueFrom:
24 | secretKeyRef:
25 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
26 | key: API_KEY
27 | - name: URL
28 | value: {{ WEB_POD_URL_INTERNAL }}/api/email_send_realtime/$(API_KEY)
29 | ---
30 | # apiVersion: batch/v1beta1
31 | # kind: CronJob
32 | # metadata:
33 | # name: {{ PROJECT }}-{{ APP_ENV }}-mailer-batch
34 | # labels:
35 | # cron-job: {{ PROJECT }}-{{ APP_ENV }}-mailer-batch
36 | # spec:
37 | # schedule: "*/10 * * * *"
38 | # successfulJobsHistoryLimit: 1
39 | # failedJobsHistoryLimit: 1
40 | # concurrencyPolicy: Forbid
41 | # startingDeadlineSeconds: 60
42 | # jobTemplate:
43 | # spec:
44 | # template:
45 | # spec:
46 | # restartPolicy: OnFailure
47 | # containers:
48 | # - name: k8s-cronjob
49 | # image: k8s-cronjob:current
50 | # env:
51 | # env:
52 | # - name: API_KEY
53 | # valueFrom:
54 | # secretKeyRef:
55 | # name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
56 | # key: API_KEY
57 | # - name: URL
58 | # value: {{ WEB_POD_URL_INTERNAL }}/api/email_send_batch/$(API_KEY)
59 | ---
60 |
--------------------------------------------------------------------------------
/sf/src/Kernel.php:
--------------------------------------------------------------------------------
1 | import('../config/{packages}/*.yaml');
17 | $container->import('../config/{packages}/'.$this->environment.'/*.yaml');
18 |
19 | if (is_file(\dirname(__DIR__).'/config/services.yaml')) {
20 | $container->import('../config/services.yaml');
21 | $container->import('../config/{services}_'.$this->environment.'.yaml');
22 | } elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) {
23 | (require $path)($container->withPath($path), $this);
24 | }
25 | }
26 |
27 | protected function configureRoutes(RoutingConfigurator $routes): void
28 | {
29 | $routes->import('../config/{routes}/'.$this->environment.'/*.yaml');
30 | $routes->import('../config/{routes}/*.yaml');
31 |
32 | if (is_file(\dirname(__DIR__).'/config/routes.yaml')) {
33 | $routes->import('../config/routes.yaml');
34 | } elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) {
35 | (require $path)($routes->withPath($path), $this);
36 | }
37 | }
38 |
39 | public function getCacheDir()
40 | {
41 | // return dirname(__DIR__).'/var/'.$this->environment.'/cache';
42 | return dirname(__DIR__).'/../cache/'.$this->environment;
43 | }
44 |
45 | public function getLogDir()
46 | {
47 | // return dirname(__DIR__).'/var/'.$this->environment.'/log';
48 | return dirname(__DIR__).'/../log/'.$this->environment;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/conf/k8s/deployment.local.tpl.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ PROJECT }}-{{ APP_ENV }}
5 | labels:
6 | app: {{ PROJECT }}-{{ APP_ENV }}
7 | spec:
8 | replicas: 1
9 | strategy:
10 | type: Recreate
11 | selector:
12 | matchLabels:
13 | app: {{ PROJECT }}-{{ APP_ENV }}
14 |
15 | template:
16 | metadata:
17 | labels:
18 | app: {{ PROJECT }}-{{ APP_ENV }}
19 | APP_ENV: {{ APP_ENV }}
20 | tag: {{ TAG }}
21 | spec:
22 |
23 | containers:
24 | - name: {{ PROJECT }}-{{ APP_ENV }}-web
25 | image: {{ PROJECT }}-{{ APP_ENV }}-web:{{ TAG }}
26 | env:
27 | - name: SYMFONY_DECRYPTION_SECRET
28 | valueFrom:
29 | secretKeyRef:
30 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
31 | key: SYMFONY_DECRYPTION_SECRET
32 | - name: API_KEY
33 | valueFrom:
34 | secretKeyRef:
35 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
36 | key: API_KEY
37 | - name: MAILER_PASSWORD
38 | valueFrom:
39 | secretKeyRef:
40 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
41 | key: MAILER_PASSWORD
42 | ports:
43 | - name: {{ PROJECT }}-{{ APP_ENV }}
44 | containerPort: 80
45 | volumeMounts:
46 | - name: {{ PROJECT }}-{{ APP_ENV }}-log
47 | mountPath: /var/www/log
48 | # Only for local development
49 | - name: {{ PROJECT }}-{{ APP_ENV }}-local-storage
50 | mountPath: /var/www/local-storage
51 | - name: {{ PROJECT }}-{{ APP_ENV }}-sf
52 | mountPath: /var/www/sf
53 |
54 | volumes:
55 | - name: {{ PROJECT }}-{{ APP_ENV }}-log
56 | emptyDir: {}
57 | - name: {{ PROJECT }}-{{ APP_ENV }}-local-storage
58 | hostPath:
59 | path: {{ HOST_DATA_LOCAL_STORAGE }}
60 | type: Directory
61 | - name: {{ PROJECT }}-{{ APP_ENV }}-sf
62 | hostPath:
63 | path: {{ HOST_SF_DIR }}
64 | type: Directory
65 |
--------------------------------------------------------------------------------
/conf/docker/Dockerfile.webinit:
--------------------------------------------------------------------------------
1 | FROM ubuntu:20.04
2 |
3 | # To avoid interactive install for git
4 | ARG DEBIAN_FRONTEND=noninteractive
5 | # Git credentials
6 | ARG GITHUB_SSH_KEY
7 | ENV GITHUB_SSH_KEY $GITHUB_SSH_KEY
8 | ARG GITHUB_ACCOUNT
9 | ENV GITHUB_ACCOUNT $GITHUB_ACCOUNT
10 | ARG GITHUB_REPO
11 | ENV GITHUB_REPO $GITHUB_REPO
12 | # Application build parameters
13 | ARG TAG
14 | ENV TAG $TAG
15 | ARG APP_ENV
16 | ENV APP_ENV $APP_ENV
17 | ARG HOST_ENV
18 | ENV HOST_ENV $HOST_ENV
19 |
20 | # Build tools
21 | RUN apt-get update \
22 | && apt-get install -y \
23 | curl \
24 | git \
25 | libssl-dev \
26 | libxml2-dev \
27 | nano \
28 | php-apcu \
29 | php-cli \
30 | php-curl \
31 | php-mbstring \
32 | php-xml \
33 | php-zip \
34 | unzip \
35 | wget
36 |
37 | WORKDIR /root
38 |
39 | # Clone the Github repo
40 | COPY infra/ssh_config .ssh/config
41 | RUN chmod 600 .ssh/config
42 | RUN echo "${GITHUB_SSH_KEY}" > .ssh/id_rsa
43 | RUN chmod 400 .ssh/id_rsa
44 | RUN git clone git@github.com:${GITHUB_ACCOUNT}/${GITHUB_REPO}.git
45 |
46 | # Fetch the version tag to build
47 | WORKDIR /root/${GITHUB_REPO}
48 | RUN git fetch --all --tags --prune \
49 | && git checkout tags/${TAG}
50 |
51 | # Copy Symfony application files
52 | RUN mkdir -p /var/www/sf
53 | WORKDIR /root/${GITHUB_REPO}/sf
54 | RUN cp -r -t /var/www/sf \
55 | bin \
56 | config \
57 | public \
58 | src \
59 | templates \
60 | translations \
61 | composer.json \
62 | composer.lock \
63 | symfony.lock
64 | COPY .env /var/www/sf/.env
65 |
66 | # Campaign email template workaround - TECHNICAL DEBT
67 | RUN mkdir -p /var/www/sf/templates/email/campaign
68 |
69 | # Composer update
70 | WORKDIR /root
71 | RUN wget -O composer-setup.php https://getcomposer.org/installer \
72 | && php composer-setup.php --install-dir=/usr/local/bin --filename=composer
73 | WORKDIR /var/www/sf
74 | RUN composer update --no-scripts
75 |
76 | RUN mkdir -p /var/www/cache \
77 | && env DB_PASSWORD="dummy" \
78 | API_KEY="dummy" \
79 | APP_SECRET="dummy" \
80 | MAILER_PASSWORD="dummy" \
81 | EMAIL_FROM="dummy@dummy.com" \
82 | /var/www/sf/bin/console cache:warmup --env=${APP_ENV}
83 |
--------------------------------------------------------------------------------
/conf/docker/Dockerfile.phpunit:
--------------------------------------------------------------------------------
1 | FROM php:7.4-cli
2 | LABEL maintainer=
3 |
4 | # Install PHP modules
5 | RUN apt-get update \
6 | # generic libraries
7 | && apt-get install -y \
8 | curl \
9 | libonig-dev \
10 | libssl-dev \
11 | libxml2-dev \
12 | libyaml-dev \
13 | nano \
14 | # PHP modules
15 | && docker-php-ext-install \
16 | exif \
17 | gettext \
18 | intl \
19 | mbstring \
20 | pdo_mysql \
21 | # APCu
22 | && echo '' | pecl install apcu-5.1.18 \
23 | && docker-php-ext-enable apcu \
24 | && echo "extension=apcu.so" > /usr/local/etc/php/php.ini \
25 | # gd
26 | && apt-get install -y \
27 | libfreetype6-dev \
28 | libjpeg62-turbo-dev \
29 | libpng-dev \
30 | && docker-php-ext-configure gd \
31 | --with-freetype \
32 | --with-jpeg \
33 | && docker-php-ext-install -j$(nproc) gd \
34 | # HTMLdoc
35 | && apt-get install -y htmldoc \
36 | # imagemagick
37 | && apt-get install -y --force-yes \
38 | libmagickwand-dev --no-install-recommends \
39 | # Inkscape is needed if you want to read SVG files created by Inkscape
40 | inkscape \
41 | && pecl install imagick-3.4.4 \
42 | && docker-php-ext-enable imagick \
43 | # imap
44 | && apt-get install -y libc-client-dev libkrb5-dev \
45 | && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
46 | # xslt
47 | && apt-get install -y libxslt-dev \
48 | && docker-php-ext-install xsl \
49 | # YAML extension
50 | && pecl install yaml-2.0.4 && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/ext-yaml.ini \
51 | # zip
52 | && apt-get install -y \
53 | libzip-dev \
54 | zip \
55 | && docker-php-ext-install zip \
56 | # Purge apt
57 | && apt-get clean -y && apt-get autoclean -y && apt-get autoremove -y \
58 | && rm -rf /var/lib/apt/lists/*
59 |
60 | # Geolite2 data file
61 | COPY conf/docker/files/geolite2/GeoLite2-Country.mmdb /usr/local/share/geolite2/GeoLite2-Country.mmdb
62 |
63 | # Install Composer
64 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
65 |
66 | WORKDIR "/var/www/sf"
67 | ENTRYPOINT ["./bin/phpunit"]
68 |
--------------------------------------------------------------------------------
/sf/config/packages/security.yaml:
--------------------------------------------------------------------------------
1 | security:
2 | providers:
3 | db:
4 | entity:
5 | class: App\Entity\User
6 | property: email
7 | firewalls:
8 | # for the time being, completely open API resources
9 | api:
10 | pattern: ^/api
11 | stateless: true
12 | anonymous: true
13 | security: false
14 |
15 | # disables authentication for assets and the profiler, adapt it according to your needs
16 | dev:
17 | pattern: ^/(_(profiler|wdt)|css|images|js)/
18 | security: false
19 |
20 | main:
21 | anonymous: true
22 | lazy: true
23 | provider: db
24 | form_login:
25 | login_path: /
26 | check_path: /login
27 | username_parameter: _email
28 | password_parameter: _password
29 | success_handler: App\Security\AuthenticationSuccessHandler
30 | failure_handler: App\Security\AuthenticationFailureHandler
31 | logout:
32 | path: /logout
33 | invalidate_session: true
34 | remember_me:
35 | secret: '%env(APP_SECRET)%'
36 | path: /
37 | always_remember_me: true
38 | switch_user: { role: ROLE_ADMIN, parameter: _su }
39 | user_checker: App\Security\UserChecker
40 | # guard:
41 | # authenticators:
42 | # - App\Security\PasswordResetAuthenticator
43 | #provider: users_in_memory
44 |
45 | # Order is critical: first matched URL applies, so order by more specific first
46 | access_control:
47 | # Admin section
48 | - { path: '^/admin/', roles: [ROLE_ADMIN] }
49 | - { path: '^/admin', roles: [IS_AUTHENTICATED_ANONYMOUSLY] }
50 | # User section
51 | - { path: '^/user/', roles: [ROLE_CLIENTUSER] }
52 | - { path: '^/user', roles: [IS_AUTHENTICATED_ANONYMOUSLY] }
53 | # Home page
54 | - { path: '^/', roles: [IS_AUTHENTICATED_ANONYMOUSLY] }
55 | # https://symfony.com/doc/current/security.html#firewalls-authentication
56 | role_hierarchy:
57 | ROLE_CLIENTADMIN: [ROLE_CLIENTUSER]
58 | # https://symfony.com/doc/current/security/impersonating_user.html
59 | # switch_user: true
60 | encoders:
61 | App\Entity\User:
62 | algorithm: auto
63 |
--------------------------------------------------------------------------------
/conf/k8s/cronjob.remote.tpl.yml:
--------------------------------------------------------------------------------
1 | apiVersion: batch/v1beta1
2 | kind: CronJob
3 | metadata:
4 | name: {{ PROJECT }}-{{ APP_ENV }}-mailer-realtime
5 | labels:
6 | cron-job: {{ PROJECT }}-{{ APP_ENV }}-mailer-realtime
7 | spec:
8 | schedule: "*/2 * * * *"
9 | successfulJobsHistoryLimit: 0
10 | failedJobsHistoryLimit: 0
11 | concurrencyPolicy: Forbid
12 | startingDeadlineSeconds: 60
13 | jobTemplate:
14 | spec:
15 | template:
16 | spec:
17 | restartPolicy: OnFailure
18 | containers:
19 | - name: k8s-cronjob
20 | image: us-central1-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/k8s-cronjob:current
21 | env:
22 | - name: API_KEY
23 | valueFrom:
24 | secretKeyRef:
25 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
26 | key: API_KEY
27 | - name: URL
28 | value: {{ WEB_POD_URL_INTERNAL }}/api/email_send_realtime/$(API_KEY)
29 | resources:
30 | requests:
31 | memory: "512Mi"
32 | cpu: "250m"
33 | limits:
34 | memory: "512Mi"
35 | cpu: "250m"
36 | ---
37 | # apiVersion: batch/v1beta1
38 | # kind: CronJob
39 | # metadata:
40 | # name: {{ PROJECT }}-{{ APP_ENV }}-mailer-batch
41 | # labels:
42 | # cron-job: {{ PROJECT }}-{{ APP_ENV }}-mailer-batch
43 | # spec:
44 | # schedule: "*/10 * * * *"
45 | # successfulJobsHistoryLimit: 0
46 | # failedJobsHistoryLimit: 0
47 | # concurrencyPolicy: Forbid
48 | # startingDeadlineSeconds: 60
49 | # jobTemplate:
50 | # spec:
51 | # template:
52 | # spec:
53 | # restartPolicy: OnFailure
54 | # containers:
55 | # - name: k8s-cronjob
56 | # image: us-central1-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/k8s-cronjob:current
57 | # env:
58 | # - name: API_KEY
59 | # valueFrom:
60 | # secretKeyRef:
61 | # name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
62 | # key: API_KEY
63 | # - name: URL
64 | # value: {{ WEB_POD_URL_INTERNAL }}/api/email_send_batch/$(API_KEY)
65 | # resources:
66 | # requests:
67 | # memory: "512Mi"
68 | # cpu: "250m"
69 | # limits:
70 | # memory: "512Mi"
71 | # cpu: "250m"
72 |
--------------------------------------------------------------------------------
/conf/k8s/deployment.remote.tpl.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ PROJECT }}-{{ APP_ENV }}
5 | labels:
6 | app: {{ PROJECT }}-{{ APP_ENV }}
7 | spec:
8 | replicas: 1
9 | strategy:
10 | type: Recreate
11 | selector:
12 | matchLabels:
13 | app: {{ PROJECT }}-{{ APP_ENV }}
14 |
15 | template:
16 | metadata:
17 | labels:
18 | app: {{ PROJECT }}-{{ APP_ENV }}
19 | APP_ENV: {{ APP_ENV }}
20 | tag: "{{ TAG }}"
21 | spec:
22 |
23 | initContainers:
24 | - name: {{ PROJECT }}-{{ APP_ENV }}-webinit
25 | image: {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ PROJECT }}-{{ APP_ENV }}-webinit:{{ TAG }}
26 | command:
27 | - "/bin/sh"
28 | - "-c"
29 | - |
30 | cp -r /var/www/sf/* /sf \
31 | && cp -r /var/www/sf/.env /sf/.env \
32 | && cp -r /var/www/cache/* /cache \
33 | && chown -R 1000:33 /cache \
34 | && chown -R 1000:33 /sf
35 | # && chmod 0777 /sf/templates/email/campaign \
36 | volumeMounts:
37 | - name: sf
38 | mountPath: /sf
39 | - name: cache
40 | mountPath: /cache
41 |
42 | containers:
43 | - name: {{ PROJECT }}-{{ APP_ENV }}-web
44 | image: {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ PROJECT }}-{{ APP_ENV }}-web:{{ TAG }}
45 | env:
46 | - name: SYMFONY_DECRYPTION_SECRET
47 | valueFrom:
48 | secretKeyRef:
49 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
50 | key: SYMFONY_DECRYPTION_SECRET
51 | - name: API_KEY
52 | valueFrom:
53 | secretKeyRef:
54 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
55 | key: API_KEY
56 | - name: MAILER_PASSWORD
57 | valueFrom:
58 | secretKeyRef:
59 | name: {{ PROJECT }}-{{ APP_ENV }}-sf-secrets
60 | key: MAILER_PASSWORD
61 | ports:
62 | - name: {{ PROJECT }}-{{ APP_ENV }}
63 | containerPort: 80
64 | resources:
65 | requests:
66 | memory: "512Mi"
67 | cpu: "250m"
68 | limits:
69 | memory: "512Mi"
70 | cpu: "250m"
71 | volumeMounts:
72 | - name: sf
73 | mountPath: /var/www/sf
74 | - name: cache
75 | mountPath: /var/www/cache
76 |
77 | volumes:
78 | - name: sf
79 | emptyDir: {}
80 | - name: cache
81 | emptyDir: {}
82 |
--------------------------------------------------------------------------------
/sf/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "project",
3 | "license": "proprietary",
4 | "require": {
5 | "php": "^7.4",
6 | "ext-ctype": "*",
7 | "ext-iconv": "*",
8 | "craue/formflow-bundle": "~3.2",
9 | "geoip2/geoip2": "~2.0",
10 | "laminas/laminas-code": "^3.4",
11 | "laminas/laminas-escaper": "^2.6",
12 | "laminas/laminas-eventmanager": "^3.2",
13 | "league/flysystem-bundle": "^1.5",
14 | "mossadal/math-parser": "^1.3",
15 | "phpoffice/phpword": "^0.17.0",
16 | "ramsey/uuid-doctrine": "^1.6",
17 | "sensio/framework-extra-bundle": "^5.1",
18 | "superbalist/flysystem-google-storage": "^7.2",
19 | "symfony/asset": "5.2.*",
20 | "symfony/console": "5.2.*",
21 | "symfony/dotenv": "5.2.*",
22 | "symfony/expression-language": "5.2.*",
23 | "symfony/flex": "^1.1",
24 | "symfony/form": "5.2.*",
25 | "symfony/framework-bundle": "5.2.*",
26 | "symfony/intl": "5.2.*",
27 | "symfony/mailer": "5.2.*",
28 | "symfony/messenger": "5.2.*",
29 | "symfony/monolog-bundle": "^3.1",
30 | "symfony/orm-pack": "^2.1",
31 | "symfony/process": "5.2.*",
32 | "symfony/security-bundle": "5.2.*",
33 | "symfony/serializer-pack": "^1.0",
34 | "symfony/translation": "5.2.*",
35 | "symfony/twig-bundle": "5.2.*",
36 | "symfony/uid": "5.2.*",
37 | "symfony/validator": "5.2.*",
38 | "symfony/web-link": "5.2.*",
39 | "symfony/yaml": "5.2.*",
40 | "twig/cssinliner-extra": "^3.0",
41 | "twig/extra-bundle": "^3.0",
42 | "ua-parser/uap-php": "^3.9"
43 | },
44 | "require-dev": {
45 | "dama/doctrine-test-bundle": "^6.5",
46 | "symfony/debug-pack": "*",
47 | "symfony/maker-bundle": "^1.0",
48 | "symfony/phpunit-bridge": "^5.2",
49 | "symfony/profiler-pack": "*",
50 | "symfony/test-pack": "*"
51 | },
52 | "config": {
53 | "preferred-install": {
54 | "*": "dist"
55 | },
56 | "sort-packages": true,
57 | "platform": {
58 | "php": "7.4"
59 | }
60 | },
61 | "autoload": {
62 | "psr-4": {
63 | "App\\": "src/"
64 | }
65 | },
66 | "autoload-dev": {
67 | "psr-4": {
68 | "App\\Tests\\": "tests/"
69 | }
70 | },
71 | "scripts": {
72 | "auto-scripts": {
73 | "cache:clear": "symfony-cmd",
74 | "assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd",
75 | "assets:install %PUBLIC_DIR%": "symfony-cmd"
76 | },
77 | "post-install-cmd": [
78 | "@auto-scripts"
79 | ],
80 | "post-update-cmd": [
81 | "@auto-scripts"
82 | ]
83 | },
84 | "conflict": {
85 | "symfony/symfony": "*"
86 | },
87 | "extra": {
88 | "symfony": {
89 | "id": "01C0DG6SEZ39ABT6Y5YM8T4HNA",
90 | "allow-contrib": false,
91 | "require": "5.2.*"
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "f7a5b0c01b4bcbe9cbb0ae3b47394e5a",
8 | "packages": [
9 | {
10 | "name": "symfony/dotenv",
11 | "version": "v4.4.18",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/symfony/dotenv.git",
15 | "reference": "2befc49ec50b4d6ffd100b332389260c9069ba1c"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/symfony/dotenv/zipball/2befc49ec50b4d6ffd100b332389260c9069ba1c",
20 | "reference": "2befc49ec50b4d6ffd100b332389260c9069ba1c",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": ">=7.1.3"
25 | },
26 | "require-dev": {
27 | "symfony/process": "^3.4.2|^4.0|^5.0"
28 | },
29 | "type": "library",
30 | "autoload": {
31 | "psr-4": {
32 | "Symfony\\Component\\Dotenv\\": ""
33 | },
34 | "exclude-from-classmap": [
35 | "/Tests/"
36 | ]
37 | },
38 | "notification-url": "https://packagist.org/downloads/",
39 | "license": [
40 | "MIT"
41 | ],
42 | "authors": [
43 | {
44 | "name": "Fabien Potencier",
45 | "email": "fabien@symfony.com"
46 | },
47 | {
48 | "name": "Symfony Community",
49 | "homepage": "https://symfony.com/contributors"
50 | }
51 | ],
52 | "description": "Registers environment variables from a .env file",
53 | "homepage": "https://symfony.com",
54 | "keywords": [
55 | "dotenv",
56 | "env",
57 | "environment"
58 | ],
59 | "support": {
60 | "source": "https://github.com/symfony/dotenv/tree/v4.4.18"
61 | },
62 | "funding": [
63 | {
64 | "url": "https://symfony.com/sponsor",
65 | "type": "custom"
66 | },
67 | {
68 | "url": "https://github.com/fabpot",
69 | "type": "github"
70 | },
71 | {
72 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
73 | "type": "tidelift"
74 | }
75 | ],
76 | "time": "2020-12-08T16:59:59+00:00"
77 | }
78 | ],
79 | "packages-dev": [],
80 | "aliases": [],
81 | "minimum-stability": "stable",
82 | "stability-flags": [],
83 | "prefer-stable": false,
84 | "prefer-lowest": false,
85 | "platform": [],
86 | "platform-dev": [],
87 | "plugin-api-version": "2.0.0"
88 | }
89 |
--------------------------------------------------------------------------------
/sf/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | tests
56 |
57 |
58 |
59 |
60 |
61 | src
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
72 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/sf/config/services.yaml:
--------------------------------------------------------------------------------
1 | # This file is the entry point to configure your own services.
2 | # Files in the packages/ subdirectory configure your dependencies.
3 |
4 | # Put parameters here that don't need to change on each machine where the app is deployed
5 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
6 | parameters:
7 | # Symfony standard
8 | httpProtocol: '%env(ROUTER_REQUEST_CONTEXT_SCHEME)%'
9 | locale: 'en'
10 | router.request_context.host: '%env(ROUTER_REQUEST_CONTEXT_HOST)%'
11 | router.request_context.scheme: '%env(ROUTER_REQUEST_CONTEXT_SCHEME)%'
12 | secret: '%env(APP_SECRET)%'
13 | api_key: '%env(API_KEY)%'
14 | email_from: '%env(EMAIL_FROM)%'
15 |
16 | # Storage
17 | storage.public.adapter: '%env(STORAGE_PUBLIC_ADAPTER)%'
18 | storage.private.adapter: '%env(STORAGE_PRIVATE_ADAPTER)%'
19 | storage_adapter: '%env(STORAGE_ADAPTER)%'
20 | cdn_url: '%env(CDN_URL)%'
21 | cdn_url_from_cluster: '%env(CDN_URL_FROM_CLUSTER)%'
22 |
23 | # App custom
24 | page_size: 10
25 |
26 | # asset cache busting
27 | asset_v: '20200907'
28 |
29 | services:
30 | # default configuration for services in *this* file
31 | _defaults:
32 | autowire: true # Automatically injects dependencies in your services.
33 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
34 | public: false # Allows optimizing the container by removing unused services; this also means
35 | # fetching services directly from the container via $container->get() won't work.
36 | # The best practice is to be explicit about your dependencies anyway.
37 | bind:
38 | $projectDir: '%kernel.project_dir%'
39 | $api_key: '%api_key%'
40 | $email_from: '%email_from%'
41 | # $mailer_batch: '@mailer.messenger.batch'
42 | # $mailer_realtime: '@mailer.messenger.realtime'
43 |
44 | # makes classes in src/ available to be used as services
45 | # this creates a service per class whose id is the fully-qualified class name
46 | App\:
47 | resource: '../src/*'
48 | exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
49 |
50 | # controllers are imported separately to make sure services can be injected
51 | # as action arguments even if you don't extend any base controller class
52 | App\Controller\:
53 | resource: '../src/Controller'
54 | tags: ['controller.service_arguments']
55 |
56 | # add more service definitions when explicit configuration is needed
57 | # please note that last definitions always *replace* previous ones
58 |
59 | # Success handler after authentication - sets session variables user, user_name, company, company_name, log audit event
60 | App\Security\AuthenticationSuccessHandler: ~
61 |
62 | # Failure handler after authentication - log audit event
63 | App\Security\AuthenticationFailureHandler: ~
64 |
65 | # Logout event listener
66 | App\EventListener\LogoutListener:
67 | tags:
68 | - name: 'kernel.event_listener'
69 | event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
70 | dispatcher: security.event_dispatcher.main
71 |
72 | # Authentication event listener
73 | App\EventListener\AuthenticationListener:
74 | tags:
75 | - { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
76 | - { name: kernel.event_listener, event: security.interactive_login, method: onAuthenticationSuccess }
77 |
78 | # Switch user event listener
79 | App\EventListener\SwitchUserListener:
80 | tags:
81 | - { name: kernel.event_listener, event: security.switch_user, method: onSwitchUser }
82 |
83 | # To manually create a RememberMe cookie after programmatic login
84 | Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices:
85 | public: true
86 | arguments:
87 | - ['@security.authentication_utils']
88 | - '%env(APP_SECRET)%'
89 | - 'db'
90 | - { name: 'REMEMBERME', lifetime: 31536000, path: '/', domain: ~, always_remember_me: true, remember_me_parameter: '_remember_me' }
91 | # 'name' => 'REMEMBERME',
92 | # 'lifetime' => 31536000,
93 | # 'path' => '/',
94 | # 'domain' => null,
95 | # 'secure' => false,
96 | # 'httponly' => true,
97 | # 'always_remember_me' => false,
98 | # 'remember_me_parameter' => '_remember_me',
99 |
100 | # user_update.listener:
101 | App\EventListener\UserUpdateListener:
102 | tags:
103 | - { name: doctrine.event_listener, event: preUpdate }
104 |
105 | # Google Cloud storage (Flysystem)
106 | Google\Cloud\Storage\StorageClient: ~
107 |
--------------------------------------------------------------------------------
/conf/docker/Dockerfile.web.remote:
--------------------------------------------------------------------------------
1 | FROM php:7.4-apache
2 | LABEL maintainer=
3 |
4 | # Install PHP modules
5 | RUN apt-get update \
6 | # generic libraries
7 | && apt-get install -y \
8 | curl \
9 | libonig-dev \
10 | libssl-dev \
11 | libxml2-dev \
12 | libyaml-dev \
13 | nano \
14 | # PHP modules
15 | && docker-php-ext-configure pcntl --enable-pcntl \
16 | && docker-php-ext-install \
17 | exif \
18 | gettext \
19 | intl \
20 | mbstring \
21 | pcntl \
22 | pdo_mysql \
23 | # APCu
24 | && echo '' | pecl install apcu-5.1.18 \
25 | && docker-php-ext-enable apcu \
26 | && echo "extension=apcu.so" > /usr/local/etc/php/php.ini \
27 | # gd
28 | && apt-get install -y \
29 | libfreetype6-dev \
30 | libjpeg62-turbo-dev \
31 | libpng-dev \
32 | && docker-php-ext-configure gd \
33 | --with-freetype \
34 | --with-jpeg \
35 | && docker-php-ext-install -j$(nproc) gd \
36 | # HTMLdoc
37 | && apt-get install -y htmldoc \
38 | # imagemagick
39 | && apt-get install -y --force-yes \
40 | libmagickwand-dev --no-install-recommends \
41 | # Inkscape is needed if you want to read SVG files created by Inkscape
42 | inkscape \
43 | && pecl install imagick-3.4.4 \
44 | && docker-php-ext-enable imagick \
45 | # imap
46 | && apt-get install -y libc-client-dev libkrb5-dev \
47 | && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
48 | # xslt
49 | && apt-get install -y libxslt-dev \
50 | && docker-php-ext-install xsl \
51 | # YAML extension
52 | && pecl install yaml-2.0.4 && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/ext-yaml.ini \
53 | # zip
54 | && apt-get install -y \
55 | libzip-dev \
56 | zip \
57 | && docker-php-ext-install zip \
58 | # Purge apt
59 | && apt-get clean -y && apt-get autoclean -y && apt-get autoremove -y \
60 | && rm -rf /var/lib/apt/lists/*
61 |
62 | # Opcache
63 | RUN docker-php-ext-install opcache
64 |
65 | RUN ln -snf /usr/share/zoneinfo/NZ /etc/localtime
66 |
67 | # Dockerize
68 | RUN curl -sfL https://github.com/powerman/dockerize/releases/download/v0.10.0/dockerize-`uname -s`-`uname -m` \
69 | | install /dev/stdin /usr/local/bin/dockerize
70 |
71 | # PHP CLI configuration
72 | COPY infra/php.ini.tpl /usr/local/etc/php/php.ini.tpl
73 |
74 | # Geolite2 data file
75 | COPY files/geolite2/GeoLite2-Country.mmdb /usr/local/share/geolite2/GeoLite2-Country.mmdb
76 |
77 | # Install Apache modules
78 | ENV APACHE_RUN_USER www-data
79 | ENV APACHE_RUN_GROUP www-data
80 | ENV APACHE_LOG_DIR /var/log/apache2
81 | ENV APACHE_PID_FILE /var/run/apache2/apache2.pid
82 | ENV APACHE_RUN_DIR /var/run/apache2
83 | ENV APACHE_LOCK_DIR /var/lock/apache2
84 | ENV APACHE_LOG_DIR /var/log/apache2
85 | RUN a2enmod rewrite \
86 | && a2enmod ssl \
87 | && a2dissite 000-default.conf \
88 | && chown -R www-data:www-data /var/www \
89 | && mkdir -p $APACHE_RUN_DIR \
90 | && mkdir -p $APACHE_LOCK_DIR \
91 | && mkdir -p $APACHE_LOG_DIR
92 |
93 | # Apache configuration (project-dependent)
94 | COPY infra/php.ini.tpl /etc/php/7.4/apache2/php.ini.tpl
95 | COPY infra/virtual-host.conf.tpl /etc/apache2/sites-enabled/virtual-host.conf.tpl
96 |
97 | # Sendmail installation
98 | RUN apt-get update \
99 | && apt-get install -y \
100 | msmtp msmtp-mta \
101 | # Purge apt
102 | && apt-get clean -y && apt-get autoclean -y && apt-get autoremove -y \
103 | && rm -rf /var/lib/apt/lists/*
104 |
105 | # MSMTP configuration (project-dependent)
106 | COPY infra/msmtprc.tpl /etc/msmtprc.tpl
107 | COPY infra/msmtp.logrotate /etc/logrotate.d/msmtp
108 |
109 | # Arguments to build configuration files with dockerize
110 | ARG ROUTER_REQUEST_CONTEXT_HOST
111 | ENV ROUTER_REQUEST_CONTEXT_HOST $ROUTER_REQUEST_CONTEXT_HOST
112 | ARG PHP_DISPLAY_ERRORS
113 | ENV PHP_DISPLAY_ERRORS $PHP_DISPLAY_ERRORS
114 | ARG PHP_ERROR_REPORTING
115 | ENV PHP_ERROR_REPORTING $PHP_ERROR_REPORTING
116 | ARG OPCACHE_ENABLE
117 | ENV OPCACHE_ENABLE $OPCACHE_ENABLE
118 | ARG OPCACHE_PRELOAD
119 | ENV OPCACHE_PRELOAD $OPCACHE_PRELOAD
120 | ARG SMTP_ACCOUNT
121 | ENV SMTP_ACCOUNT $SMTP_ACCOUNT
122 | ARG APP_ENV
123 | ENV APP_ENV $APP_ENV
124 |
125 | # To allow SF to write to var/cache and log on mounted volumes
126 | RUN usermod -u 1000 www-data
127 |
128 | # Mount points for Symfony application files from build stage
129 | RUN mkdir -p /var/www/cache \
130 | && chown -R www-data:www-data /var/www/cache \
131 | && mkdir -p /var/www/log \
132 | && chown -R www-data:www-data /var/www/log \
133 | && mkdir -p /var/www/sf \
134 | && chown -R www-data:www-data /var/www/sf
135 |
136 | # Add Tini
137 | ENV TINI_VERSION v0.19.0
138 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
139 | RUN chmod +x /tini
140 | ENTRYPOINT ["/tini", "--", "docker-php-entrypoint"]
141 |
142 | #CMD ["apachectl", "-D", "FOREGROUND"]
143 | CMD ["dockerize", \
144 | "-template", "/usr/local/etc/php/php.ini.tpl:/usr/local/etc/php/php.ini", \
145 | "-template", "/etc/php/7.4/apache2/php.ini.tpl:/etc/php/7.4/apache2/php.ini", \
146 | "-template", "/etc/apache2/sites-enabled/virtual-host.conf.tpl:/etc/apache2/sites-enabled/virtual-host.conf", \
147 | "-template", "/etc/msmtprc.tpl:/etc/msmtprc", \
148 | "apachectl", "-D", "FOREGROUND"]
149 |
--------------------------------------------------------------------------------
/conf/docker/Dockerfile.web.local:
--------------------------------------------------------------------------------
1 | FROM php:7.4-apache
2 | LABEL maintainer=
3 |
4 | # Install PHP modules
5 | RUN apt-get update \
6 | # generic libraries
7 | && apt-get install -y \
8 | curl \
9 | libonig-dev \
10 | libssl-dev \
11 | libxml2-dev \
12 | libyaml-dev \
13 | nano \
14 | # PHP modules
15 | && docker-php-ext-configure pcntl --enable-pcntl \
16 | && docker-php-ext-install \
17 | exif \
18 | gettext \
19 | intl \
20 | mbstring \
21 | pcntl \
22 | pdo_mysql \
23 | # APCu
24 | && echo '' | pecl install apcu-5.1.18 \
25 | && docker-php-ext-enable apcu \
26 | && echo "extension=apcu.so" > /usr/local/etc/php/php.ini \
27 | # gd
28 | && apt-get install -y \
29 | libfreetype6-dev \
30 | libjpeg62-turbo-dev \
31 | libpng-dev \
32 | && docker-php-ext-configure gd \
33 | --with-freetype \
34 | --with-jpeg \
35 | && docker-php-ext-install -j$(nproc) gd \
36 | # HTMLdoc
37 | && apt-get install -y htmldoc \
38 | # imagemagick
39 | && apt-get install -y --force-yes \
40 | libmagickwand-dev --no-install-recommends \
41 | # Inkscape is needed if you want to read SVG files created by Inkscape
42 | inkscape \
43 | && pecl install imagick-3.4.4 \
44 | && docker-php-ext-enable imagick \
45 | # imap
46 | && apt-get install -y libc-client-dev libkrb5-dev \
47 | && docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
48 | # xslt
49 | && apt-get install -y libxslt-dev \
50 | && docker-php-ext-install xsl \
51 | # YAML extension
52 | && pecl install yaml-2.0.4 && echo "extension=yaml.so" > /usr/local/etc/php/conf.d/ext-yaml.ini \
53 | # zip
54 | && apt-get install -y \
55 | libzip-dev \
56 | zip \
57 | && docker-php-ext-install zip \
58 | # Purge apt
59 | && apt-get clean -y && apt-get autoclean -y && apt-get autoremove -y \
60 | && rm -rf /var/lib/apt/lists/*
61 |
62 | # Opcache
63 | RUN docker-php-ext-install opcache
64 |
65 | RUN ln -snf /usr/share/zoneinfo/NZ /etc/localtime
66 |
67 | # Dockerize
68 | RUN curl -sfL https://github.com/powerman/dockerize/releases/download/v0.10.0/dockerize-`uname -s`-`uname -m` \
69 | | install /dev/stdin /usr/local/bin/dockerize
70 |
71 | # PHP CLI configuration
72 | COPY infra/php.ini.tpl /usr/local/etc/php/php.ini.tpl
73 |
74 | # Geolite2 data file
75 | COPY files/geolite2/GeoLite2-Country.mmdb /usr/local/share/geolite2/GeoLite2-Country.mmdb
76 |
77 | # Install Apache modules
78 | ENV APACHE_RUN_USER www-data
79 | ENV APACHE_RUN_GROUP www-data
80 | ENV APACHE_LOG_DIR /var/log/apache2
81 | ENV APACHE_PID_FILE /var/run/apache2/apache2.pid
82 | ENV APACHE_RUN_DIR /var/run/apache2
83 | ENV APACHE_LOCK_DIR /var/lock/apache2
84 | ENV APACHE_LOG_DIR /var/log/apache2
85 | RUN a2enmod rewrite \
86 | && a2enmod ssl \
87 | && a2dissite 000-default.conf \
88 | && chown -R www-data:www-data /var/www \
89 | && mkdir -p $APACHE_RUN_DIR \
90 | && mkdir -p $APACHE_LOCK_DIR \
91 | && mkdir -p $APACHE_LOG_DIR
92 |
93 | # Apache configuration (project-dependent)
94 | COPY infra/php.ini.tpl /etc/php/7.4/apache2/php.ini.tpl
95 | COPY infra/virtual-host.conf.tpl /etc/apache2/sites-enabled/virtual-host.conf.tpl
96 |
97 | # Sendmail installation
98 | RUN apt-get update \
99 | && apt-get install -y \
100 | msmtp msmtp-mta \
101 | # Purge apt
102 | && apt-get clean -y && apt-get autoclean -y && apt-get autoremove -y \
103 | && rm -rf /var/lib/apt/lists/*
104 |
105 | # MSMTP configuration (project-dependent)
106 | COPY infra/msmtprc.tpl /etc/msmtprc.tpl
107 | COPY infra/msmtp.logrotate /etc/logrotate.d/msmtp
108 |
109 | # Copy Symfony application files from local context
110 | ADD sf.tar.gz /var/www/
111 |
112 | # Arguments to build configuration files with dockerize
113 | ARG ROUTER_REQUEST_CONTEXT_HOST
114 | ENV ROUTER_REQUEST_CONTEXT_HOST $ROUTER_REQUEST_CONTEXT_HOST
115 | ARG PHP_DISPLAY_ERRORS
116 | ENV PHP_DISPLAY_ERRORS $PHP_DISPLAY_ERRORS
117 | ARG PHP_ERROR_REPORTING
118 | ENV PHP_ERROR_REPORTING $PHP_ERROR_REPORTING
119 | ARG OPCACHE_ENABLE
120 | ENV OPCACHE_ENABLE $OPCACHE_ENABLE
121 | ARG OPCACHE_PRELOAD
122 | ENV OPCACHE_PRELOAD $OPCACHE_PRELOAD
123 | ARG SMTP_ACCOUNT
124 | ENV SMTP_ACCOUNT $SMTP_ACCOUNT
125 | ARG APP_ENV
126 | ENV APP_ENV $APP_ENV
127 |
128 | # To allow SF to write to var/cache and log on mounted volumes
129 | RUN usermod -u 1000 www-data
130 |
131 | # Preload Symfony cache for PHP opcache
132 | RUN mkdir -p /var/www/cache \
133 | && chown -R www-data:www-data /var/www/cache \
134 | && mkdir -p /var/www/log \
135 | && chown -R www-data:www-data /var/www/log \
136 | && env DB_PASSWORD="dummy" \
137 | API_KEY="dummy" \
138 | APP_SECRET="dummy" \
139 | MAILER_PASSWORD="dummy" \
140 | /var/www/sf/bin/console cache:warmup --env=${APP_ENV}
141 |
142 | # Add Tini
143 | ENV TINI_VERSION v0.19.0
144 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
145 | RUN chmod +x /tini
146 | ENTRYPOINT ["/tini", "--", "docker-php-entrypoint"]
147 |
148 | #CMD ["apachectl", "-D", "FOREGROUND"]
149 | CMD ["dockerize", \
150 | "-template", "/usr/local/etc/php/php.ini.tpl:/usr/local/etc/php/php.ini", \
151 | "-template", "/etc/php/7.4/apache2/php.ini.tpl:/etc/php/7.4/apache2/php.ini", \
152 | "-template", "/etc/apache2/sites-enabled/virtual-host.conf.tpl:/etc/apache2/sites-enabled/virtual-host.conf", \
153 | "-template", "/etc/msmtprc.tpl:/etc/msmtprc", \
154 | "apachectl", "-D", "FOREGROUND"]
155 |
--------------------------------------------------------------------------------
/docs/architecture.drawio:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/conf/deployer/deploy.php:
--------------------------------------------------------------------------------
1 | set('HOST_ENV', 'local')
60 | ->set('CACHE_FROM_WEB', function () {
61 | return '';
62 | })
63 | ->set('CACHE_FROM_WEBINIT', function () {
64 | return '';
65 | })
66 | ;
67 |
68 | /**
69 | * Set .env file, load into environment variables
70 | */
71 | task(
72 | 'load-env', function () {
73 | set('ENV_FILE', parse("{{ ROOT_DIR }}/conf/env/.env.{{ APP_ENV }}.{{ HOST_ENV }}"));
74 | (new Dotenv())->load(get('ENV_FILE'));
75 | array_map(
76 | function ($var) {
77 | set($var, getenv($var));
78 | },
79 | explode(',', $_SERVER['SYMFONY_DOTENV_VARS'])
80 | );
81 | }
82 | );
83 |
84 | /**
85 | * Generate service/ cronjob manifests (usually done only once)
86 | */
87 | task(
88 | 'gen-service', [
89 | // Set .env file, load into environment variables
90 | 'load-env',
91 | // Build service manifests
92 | 'service-manifest',
93 | ]
94 | );
95 |
96 | /**
97 | * Build Kubernetes service manifests
98 | */
99 | task(
100 | 'service-manifest', function () {
101 | // Create build output folder
102 | runLocally("rm -rf build && mkdir build");
103 | // Service manifests
104 | $conf_file = parse("{{ ROOT_DIR }}/conf/k8s/service.tpl.yml");
105 | $build_file = parse("{{ ROOT_DIR }}/build/service.yml");
106 | parse_into($conf_file, $build_file);
107 | // Cronjob manifests
108 | $conf_file = parse("{{ ROOT_DIR }}/conf/k8s/cronjob.{{ HOST_ENV }}.tpl.yml");
109 | $build_file = parse("{{ ROOT_DIR }}/build/cronjob.{{ HOST_ENV }}.yml");
110 | parse_into($conf_file, $build_file);
111 | }
112 | );
113 |
114 | /**
115 | * Deploy containers on localhost
116 | */
117 | task(
118 | 'deploy-local', [
119 | // Set .env file, load into environment variables
120 | 'load-env',
121 | // Set local docker context to Minikube
122 | 'local:set-docker-context',
123 | // Build docker context
124 | 'docker-build-context',
125 | // Add Symfony local working directory to docker build context
126 | 'local:add-sf-dir',
127 | // Build docker web image
128 | 'docker-build-web-image',
129 | // Apply Kubernetes deployment
130 | 'k8s-deployment',
131 | // Set local docker back to local host
132 | 'local:unset-docker-context',
133 | ]
134 | );
135 |
136 | /**
137 | * Build docker web image
138 | */
139 | task(
140 | 'docker-build-web-image', function () {
141 | cd("{{ ROOT_DIR }}/build");
142 | run(
143 | "
144 | docker build -f Dockerfile.web.{{ HOST_ENV }} \
145 | -t {{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-web:{{ TAG }} {{ CACHE_FROM_WEB }} \
146 | --build-arg APP_ENV={{ APP_ENV }} \
147 | --build-arg ROUTER_REQUEST_CONTEXT_HOST={{ ROUTER_REQUEST_CONTEXT_HOST }} \
148 | --build-arg PHP_DISPLAY_ERRORS={{ PHP_DISPLAY_ERRORS }} \
149 | --build-arg PHP_ERROR_REPORTING=\"{{ PHP_ERROR_REPORTING }}\" \
150 | --build-arg OPCACHE_ENABLE={{ OPCACHE_ENABLE }} \
151 | --build-arg OPCACHE_PRELOAD={{ OPCACHE_PRELOAD }} \
152 | --build-arg SMTP_ACCOUNT={{ SMTP_ACCOUNT }} \
153 | .
154 | ", ['timeout' => null, 'tty' => true]
155 | );
156 | }
157 | );
158 |
159 | /**
160 | * Build docker web init image (remote)
161 | */
162 | task(
163 | 'remote:docker-build-webinit-image', function () {
164 | cd("{{ ROOT_DIR }}/build");
165 | run(
166 | "
167 | docker build -f Dockerfile.webinit \
168 | -t {{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-webinit:{{ TAG }} {{ CACHE_FROM_WEBINIT }} \
169 | --build-arg APP_ENV={{ APP_ENV }} \
170 | --build-arg HOST_ENV={{ HOST_ENV }} \
171 | --build-arg TAG={{ TAG }} \
172 | --build-arg GITHUB_SSH_KEY=\"{{ GITHUB_SSH_KEY }}\" \
173 | --build-arg GITHUB_ACCOUNT=\"{{ GITHUB_ACCOUNT }}\" \
174 | --build-arg GITHUB_REPO=\"{{ GITHUB_REPO }}\" \
175 | .
176 | ", ['timeout' => null, 'tty' => true]
177 | );
178 | }
179 | );
180 |
181 | /**
182 | * Set docker local context to Minikube
183 | */
184 | task(
185 | 'local:set-docker-context', function () {
186 | runLocally(
187 | "
188 | eval $(minikube docker-env)
189 | ", ['timeout' => null, 'tty' => true]
190 | );
191 | }
192 | );
193 |
194 | /**
195 | * Set docker local context back to local host
196 | */
197 | task(
198 | 'local:unset-docker-context', function () {
199 | runLocally(
200 | "
201 | eval $(minikube docker-env -u)
202 | ", ['timeout' => null, 'tty' => true]
203 | );
204 | }
205 | );
206 |
207 | /**
208 | * Add Symfony local working directory to docker build context
209 | */
210 | task(
211 | 'local:add-sf-dir', function () {
212 | cd("{{ ROOT_DIR }}");
213 | // Copy ddocker context files
214 | runLocally(
215 | "
216 | cp {{ ENV_FILE }} sf/.env \
217 | && tar -czf build/sf.tar.gz -C . sf
218 | "
219 | );
220 | }
221 | );
222 |
223 | /**
224 | * Apply Kubernetes deployment
225 | */
226 | task(
227 | 'k8s-deployment', function () {
228 | cd("{{ ROOT_DIR }}");
229 | run(
230 | "
231 | kubectl apply -f build/deployment.yml
232 | ", ['timeout' => null, 'tty' => true]
233 | );
234 | }
235 | );
236 |
237 |
238 | /**
239 | * ------------------------------
240 | * Build and deploy services on Google Cloud Shell
241 | * ------------------------------
242 | */
243 | host('remote')
244 | ->set('HOST_ENV', 'remote')
245 | ->set('GITHUB_SSH_KEY', file_get_contents('/home/'.get('CLOUD_SHELL_USER').'/.ssh/id_rsa'))
246 | ->set('CACHE_FROM_WEB', function () {
247 | if (get('LAST')) {
248 | // --cache-from {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-web:{{ TAG }}
249 | return sprintf(
250 | '--cache-from %s-docker.pkg.dev/%s/%s-%s-web:%s',
251 | get('GCP_REGION'),
252 | get('GCP_PROJECT_ID'),
253 | get('PROJECT'),
254 | get('COMPOSE_PROJECT_NAME'),
255 | get('APP_ENV'),
256 | get('LAST')
257 | );
258 | } else {
259 | return '';
260 | }
261 | })
262 | ->set('CACHE_FROM_WEBINIT', function () {
263 | if (get('LAST')) {
264 | // --cache-from {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-webinit:{{ TAG }}
265 | return sprintf(
266 | '--cache-from %s-docker.pkg.dev/%s/%s-%s-webinit:%s',
267 | get('GCP_REGION'),
268 | get('GCP_PROJECT_ID'),
269 | get('PROJECT'),
270 | get('COMPOSE_PROJECT_NAME'),
271 | get('APP_ENV'),
272 | get('LAST')
273 | );
274 | } else {
275 | return '';
276 | }
277 | })
278 | ;
279 |
280 | /**
281 | * Deploy services on GKE
282 | */
283 | task(
284 | 'deploy-remote', [
285 | // Set .env file, load into environment variables
286 | 'load-env',
287 | // Build docker resources locally
288 | 'docker-build-context',
289 | // Build docker web image
290 | 'docker-build-web-image',
291 | // Build docker web init image
292 | 'remote:docker-build-webinit-image',
293 | // Push docker image to repository
294 | 'remote:push-image',
295 | // Apply Kubernetes deployment
296 | 'k8s-deployment',
297 | ]
298 | );
299 |
300 | /**
301 | * Tag local image for the Artifact Registry,
302 | * Push images to Artifact Registry
303 | */
304 | task(
305 | 'remote:push-image', function () {
306 | run(
307 | "
308 | docker tag {{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-webinit:{{ TAG }} \
309 | {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-webinit:{{ TAG }}
310 | ", ['timeout' => null, 'tty' => true]
311 | );
312 | run(
313 | "
314 | docker push {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-webinit:{{ TAG }}
315 | ", ['timeout' => null, 'tty' => true]
316 | );
317 | run(
318 | "
319 | docker tag {{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-web:{{ TAG }} \
320 | {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-web:{{ TAG }}
321 | ", ['timeout' => null, 'tty' => true]
322 | );
323 | run(
324 | "
325 | docker push {{ GCP_REGION }}-docker.pkg.dev/{{ GCP_PROJECT_ID }}/{{ PROJECT }}/{{ COMPOSE_PROJECT_NAME }}-{{ APP_ENV }}-web:{{ TAG }}
326 | ", ['timeout' => null, 'tty' => true]
327 | );
328 | }
329 | );
330 |
331 | /**
332 | * Create Docker build context
333 | */
334 | task(
335 | 'docker-build-context', function () {
336 | cd("{{ ROOT_DIR }}");
337 | // Copy ddocker context files
338 | runLocally(
339 | "
340 | rm -rf build \
341 | && mkdir -p build/infra \
342 | && cp -r conf/infra/* build/infra \
343 | && cp -r conf/docker/files build \
344 | && cp conf/docker/Dockerfile.* build \
345 | && cp {{ ENV_FILE }} build/.env
346 | "
347 | );
348 | // Copy the deployment manifest
349 | $conf_file = parse("{{ ROOT_DIR }}/conf/k8s/deployment.{{ HOST_ENV }}.tpl.yml");
350 | $build_file = parse("{{ ROOT_DIR }}/build/deployment.yml");
351 | parse_into($conf_file, $build_file);
352 | }
353 | );
354 |
355 | /**
356 | * Helper function - Parse a source file into a destination file
357 | *
358 | * @param string $src_filepath Source filepath
359 | * @param string $dest_filepath Destination filepath
360 | *
361 | * @return void
362 | */
363 | function parse_into($src_filepath, $dest_filepath): void
364 | {
365 | $content = file_get_contents($src_filepath);
366 | $content = parse($content);
367 | $fh = fopen($dest_filepath, 'w');
368 | fwrite($fh, $content);
369 | fclose($fh);
370 | }
371 |
--------------------------------------------------------------------------------
/sf/symfony.lock:
--------------------------------------------------------------------------------
1 | {
2 | "brick/math": {
3 | "version": "0.8.15"
4 | },
5 | "dama/doctrine-test-bundle": {
6 | "version": "4.0",
7 | "recipe": {
8 | "repo": "github.com/symfony/recipes-contrib",
9 | "branch": "master",
10 | "version": "4.0",
11 | "ref": "56eaa387b5e48ebcc7c95a893b47dfa1ad51449c"
12 | },
13 | "files": [
14 | "config/packages/test/dama_doctrine_test_bundle.yaml"
15 | ]
16 | },
17 | "doctrine/annotations": {
18 | "version": "1.0",
19 | "recipe": {
20 | "repo": "github.com/symfony/recipes",
21 | "branch": "master",
22 | "version": "1.0",
23 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
24 | },
25 | "files": [
26 | "config/routes/annotations.yaml"
27 | ]
28 | },
29 | "doctrine/doctrine-bundle": {
30 | "version": "2.0",
31 | "recipe": {
32 | "repo": "github.com/symfony/recipes",
33 | "branch": "master",
34 | "version": "2.0",
35 | "ref": "368794356c1fb634e58b38ad2addb36933f2e73e"
36 | },
37 | "files": [
38 | "config/packages/doctrine.yaml",
39 | "config/packages/prod/doctrine.yaml",
40 | "src/Entity/.gitignore",
41 | "src/Repository/.gitignore"
42 | ]
43 | },
44 | "doctrine/doctrine-migrations-bundle": {
45 | "version": "2.2",
46 | "recipe": {
47 | "repo": "github.com/symfony/recipes",
48 | "branch": "master",
49 | "version": "2.2",
50 | "ref": "baaa439e3e3179e69e3da84b671f0a3e4a2f56ad"
51 | },
52 | "files": [
53 | "config/packages/doctrine_migrations.yaml",
54 | "migrations/.gitignore"
55 | ]
56 | },
57 | "doctrine/event-manager": {
58 | "version": "1.1.0"
59 | },
60 | "doctrine/migrations": {
61 | "version": "2.2.1"
62 | },
63 | "doctrine/persistence": {
64 | "version": "1.3.7"
65 | },
66 | "doctrine/reflection": {
67 | "version": "1.2.1"
68 | },
69 | "doctrine/sql-formatter": {
70 | "version": "1.1.0"
71 | },
72 | "firebase/php-jwt": {
73 | "version": "v5.2.0"
74 | },
75 | "friendsofphp/proxy-manager-lts": {
76 | "version": "v1.0.1"
77 | },
78 | "google/auth": {
79 | "version": "v1.8.0"
80 | },
81 | "google/cloud-core": {
82 | "version": "v1.36.1"
83 | },
84 | "google/cloud-storage": {
85 | "version": "v1.20.1"
86 | },
87 | "google/crc32": {
88 | "version": "v0.1.0"
89 | },
90 | "guzzlehttp/guzzle": {
91 | "version": "6.5.3"
92 | },
93 | "guzzlehttp/promises": {
94 | "version": "v1.3.1"
95 | },
96 | "guzzlehttp/psr7": {
97 | "version": "1.6.1"
98 | },
99 | "laminas/laminas-zendframework-bridge": {
100 | "version": "1.0.4"
101 | },
102 | "league/flysystem": {
103 | "version": "1.0.67"
104 | },
105 | "league/flysystem-bundle": {
106 | "version": "1.0",
107 | "recipe": {
108 | "repo": "github.com/symfony/recipes-contrib",
109 | "branch": "master",
110 | "version": "1.0",
111 | "ref": "8ad3ded1f39fedce098e43259741f42092a1010e"
112 | }
113 | },
114 | "league/mime-type-detection": {
115 | "version": "1.5.1"
116 | },
117 | "mossadal/math-parser": {
118 | "version": "v1.3.16"
119 | },
120 | "nikic/php-parser": {
121 | "version": "v4.4.0"
122 | },
123 | "ocramius/package-versions": {
124 | "version": "1.5.1"
125 | },
126 | "pclzip/pclzip": {
127 | "version": "2.8.2"
128 | },
129 | "php": {
130 | "version": "7.3"
131 | },
132 | "phpdocumentor/reflection-common": {
133 | "version": "2.0.0"
134 | },
135 | "phpdocumentor/reflection-docblock": {
136 | "version": "5.1.0"
137 | },
138 | "phpdocumentor/type-resolver": {
139 | "version": "1.1.0"
140 | },
141 | "phpoffice/common": {
142 | "version": "0.2.9"
143 | },
144 | "phpoffice/phpword": {
145 | "version": "0.17.0"
146 | },
147 | "psr/event-dispatcher": {
148 | "version": "1.0.0"
149 | },
150 | "psr/http-client": {
151 | "version": "1.0.1"
152 | },
153 | "psr/http-message": {
154 | "version": "1.0.1"
155 | },
156 | "psr/link": {
157 | "version": "1.0.0"
158 | },
159 | "ralouphie/getallheaders": {
160 | "version": "3.0.3"
161 | },
162 | "ramsey/collection": {
163 | "version": "1.0.1"
164 | },
165 | "ramsey/uuid": {
166 | "version": "4.0.1"
167 | },
168 | "ramsey/uuid-doctrine": {
169 | "version": "1.3",
170 | "recipe": {
171 | "repo": "github.com/symfony/recipes-contrib",
172 | "branch": "master",
173 | "version": "1.3",
174 | "ref": "471aed0fbf5620b8d7f92b7a5ebbbf6c0945c27a"
175 | }
176 | },
177 | "rize/uri-template": {
178 | "version": "0.3.2"
179 | },
180 | "sensio/framework-extra-bundle": {
181 | "version": "5.2",
182 | "recipe": {
183 | "repo": "github.com/symfony/recipes",
184 | "branch": "master",
185 | "version": "5.2",
186 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
187 | },
188 | "files": [
189 | "config/packages/sensio_framework_extra.yaml"
190 | ]
191 | },
192 | "superbalist/flysystem-google-storage": {
193 | "version": "7.2.2"
194 | },
195 | "symfony/amqp-messenger": {
196 | "version": "v5.2.1"
197 | },
198 | "symfony/asset": {
199 | "version": "v4.4.7"
200 | },
201 | "symfony/browser-kit": {
202 | "version": "v5.0.7"
203 | },
204 | "symfony/cache-contracts": {
205 | "version": "v2.0.1"
206 | },
207 | "symfony/console": {
208 | "version": "5.1",
209 | "recipe": {
210 | "repo": "github.com/symfony/recipes",
211 | "branch": "master",
212 | "version": "5.1",
213 | "ref": "c6d02bdfba9da13c22157520e32a602dbee8a75c"
214 | },
215 | "files": [
216 | "bin/console"
217 | ]
218 | },
219 | "symfony/css-selector": {
220 | "version": "v5.0.7"
221 | },
222 | "symfony/debug-bundle": {
223 | "version": "4.1",
224 | "recipe": {
225 | "repo": "github.com/symfony/recipes",
226 | "branch": "master",
227 | "version": "4.1",
228 | "ref": "f8863cbad2f2e58c4b65fa1eac892ab189971bea"
229 | }
230 | },
231 | "symfony/debug-pack": {
232 | "version": "v1.0.8"
233 | },
234 | "symfony/deprecation-contracts": {
235 | "version": "v2.1.2"
236 | },
237 | "symfony/doctrine-messenger": {
238 | "version": "v5.2.1"
239 | },
240 | "symfony/dom-crawler": {
241 | "version": "v5.0.7"
242 | },
243 | "symfony/error-handler": {
244 | "version": "v4.4.7"
245 | },
246 | "symfony/event-dispatcher-contracts": {
247 | "version": "v1.1.7"
248 | },
249 | "symfony/flex": {
250 | "version": "1.0",
251 | "recipe": {
252 | "repo": "github.com/symfony/recipes",
253 | "branch": "master",
254 | "version": "1.0",
255 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e"
256 | },
257 | "files": [
258 | ".env"
259 | ]
260 | },
261 | "symfony/framework-bundle": {
262 | "version": "5.2",
263 | "recipe": {
264 | "repo": "github.com/symfony/recipes",
265 | "branch": "master",
266 | "version": "5.2",
267 | "ref": "6ec87563dcc85cd0c48856dcfbfc29610506d250"
268 | },
269 | "files": [
270 | "config/packages/cache.yaml",
271 | "config/packages/framework.yaml",
272 | "config/packages/test/framework.yaml",
273 | "config/preload.php",
274 | "config/routes/dev/framework.yaml",
275 | "config/services.yaml",
276 | "public/index.php",
277 | "src/Controller/.gitignore",
278 | "src/Kernel.php"
279 | ]
280 | },
281 | "symfony/http-client-contracts": {
282 | "version": "v2.3.1"
283 | },
284 | "symfony/mailer": {
285 | "version": "4.3",
286 | "recipe": {
287 | "repo": "github.com/symfony/recipes",
288 | "branch": "master",
289 | "version": "4.3",
290 | "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2"
291 | }
292 | },
293 | "symfony/maker-bundle": {
294 | "version": "1.0",
295 | "recipe": {
296 | "repo": "github.com/symfony/recipes",
297 | "branch": "master",
298 | "version": "1.0",
299 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
300 | }
301 | },
302 | "symfony/messenger": {
303 | "version": "4.3",
304 | "recipe": {
305 | "repo": "github.com/symfony/recipes",
306 | "branch": "master",
307 | "version": "4.3",
308 | "ref": "e9a414b113ceadbf4e52abe37bf8f1b443f06ccb"
309 | },
310 | "files": [
311 | "config/packages/messenger.yaml"
312 | ]
313 | },
314 | "symfony/mime": {
315 | "version": "v5.0.7"
316 | },
317 | "symfony/monolog-bundle": {
318 | "version": "3.3",
319 | "recipe": {
320 | "repo": "github.com/symfony/recipes",
321 | "branch": "master",
322 | "version": "3.3",
323 | "ref": "d7249f7d560f6736115eee1851d02a65826f0a56"
324 | },
325 | "files": [
326 | "config/packages/dev/monolog.yaml",
327 | "config/packages/prod/deprecations.yaml",
328 | "config/packages/prod/monolog.yaml",
329 | "config/packages/test/monolog.yaml"
330 | ]
331 | },
332 | "symfony/orm-pack": {
333 | "version": "v1.0.8"
334 | },
335 | "symfony/phpunit-bridge": {
336 | "version": "5.1",
337 | "recipe": {
338 | "repo": "github.com/symfony/recipes",
339 | "branch": "master",
340 | "version": "5.1",
341 | "ref": "cb82a2355ec62fef0e7028ac969fe86fa722feb8"
342 | },
343 | "files": [
344 | ".env.test",
345 | "bin/phpunit",
346 | "phpunit.xml.dist",
347 | "tests/bootstrap.php"
348 | ]
349 | },
350 | "symfony/polyfill-ctype": {
351 | "version": "v1.15.0"
352 | },
353 | "symfony/polyfill-intl-grapheme": {
354 | "version": "v1.22.0"
355 | },
356 | "symfony/polyfill-intl-idn": {
357 | "version": "v1.15.0"
358 | },
359 | "symfony/polyfill-intl-normalizer": {
360 | "version": "v1.17.0"
361 | },
362 | "symfony/polyfill-php73": {
363 | "version": "v1.15.0"
364 | },
365 | "symfony/polyfill-php80": {
366 | "version": "v1.17.0"
367 | },
368 | "symfony/polyfill-uuid": {
369 | "version": "v1.22.0"
370 | },
371 | "symfony/property-info": {
372 | "version": "v5.0.7"
373 | },
374 | "symfony/proxy-manager-bridge": {
375 | "version": "v5.2.1"
376 | },
377 | "symfony/redis-messenger": {
378 | "version": "v5.2.1"
379 | },
380 | "symfony/routing": {
381 | "version": "5.1",
382 | "recipe": {
383 | "repo": "github.com/symfony/recipes",
384 | "branch": "master",
385 | "version": "5.1",
386 | "ref": "b4f3e7c95e38b606eef467e8a42a8408fc460c43"
387 | },
388 | "files": [
389 | "config/packages/prod/routing.yaml",
390 | "config/packages/routing.yaml",
391 | "config/routes.yaml"
392 | ]
393 | },
394 | "symfony/security-bundle": {
395 | "version": "5.1",
396 | "recipe": {
397 | "repo": "github.com/symfony/recipes",
398 | "branch": "master",
399 | "version": "5.1",
400 | "ref": "0a4bae19389d3b9cba1ca0102e3b2bccea724603"
401 | },
402 | "files": [
403 | "config/packages/security.yaml"
404 | ]
405 | },
406 | "symfony/security-core": {
407 | "version": "v4.4.7"
408 | },
409 | "symfony/security-csrf": {
410 | "version": "v5.0.7"
411 | },
412 | "symfony/security-guard": {
413 | "version": "v4.4.7"
414 | },
415 | "symfony/security-http": {
416 | "version": "v4.4.7"
417 | },
418 | "symfony/serializer": {
419 | "version": "v5.0.7"
420 | },
421 | "symfony/serializer-pack": {
422 | "version": "v1.0.3"
423 | },
424 | "symfony/service-contracts": {
425 | "version": "v2.0.1"
426 | },
427 | "symfony/string": {
428 | "version": "v5.2.1"
429 | },
430 | "symfony/test-pack": {
431 | "version": "v1.0.6"
432 | },
433 | "symfony/translation": {
434 | "version": "3.3",
435 | "recipe": {
436 | "repo": "github.com/symfony/recipes",
437 | "branch": "master",
438 | "version": "3.3",
439 | "ref": "2ad9d2545bce8ca1a863e50e92141f0b9d87ffcd"
440 | },
441 | "files": [
442 | "config/packages/translation.yaml",
443 | "translations/.gitignore"
444 | ]
445 | },
446 | "symfony/translation-contracts": {
447 | "version": "v2.0.1"
448 | },
449 | "symfony/twig-bundle": {
450 | "version": "5.0",
451 | "recipe": {
452 | "repo": "github.com/symfony/recipes",
453 | "branch": "master",
454 | "version": "5.0",
455 | "ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d"
456 | },
457 | "files": [
458 | "config/packages/test/twig.yaml",
459 | "config/packages/twig.yaml",
460 | "templates/base.html.twig"
461 | ]
462 | },
463 | "symfony/uid": {
464 | "version": "v5.2.1"
465 | },
466 | "symfony/validator": {
467 | "version": "4.3",
468 | "recipe": {
469 | "repo": "github.com/symfony/recipes",
470 | "branch": "master",
471 | "version": "4.3",
472 | "ref": "d902da3e4952f18d3bf05aab29512eb61cabd869"
473 | },
474 | "files": [
475 | "config/packages/test/validator.yaml",
476 | "config/packages/validator.yaml"
477 | ]
478 | },
479 | "symfony/var-exporter": {
480 | "version": "v5.0.7"
481 | },
482 | "symfony/web-link": {
483 | "version": "v4.4.7"
484 | },
485 | "symfony/web-profiler-bundle": {
486 | "version": "3.3",
487 | "recipe": {
488 | "repo": "github.com/symfony/recipes",
489 | "branch": "master",
490 | "version": "3.3",
491 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6"
492 | },
493 | "files": [
494 | "config/packages/dev/web_profiler.yaml",
495 | "config/packages/test/web_profiler.yaml",
496 | "config/routes/dev/web_profiler.yaml"
497 | ]
498 | },
499 | "tijsverkoyen/css-to-inline-styles": {
500 | "version": "2.2.2"
501 | },
502 | "twig/cssinliner-extra": {
503 | "version": "v3.0.3"
504 | },
505 | "twig/extra-bundle": {
506 | "version": "v3.0.3"
507 | },
508 | "webmozart/assert": {
509 | "version": "1.8.0"
510 | },
511 | "zendframework/zend-code": {
512 | "version": "3.4.1"
513 | },
514 | "zendframework/zend-escaper": {
515 | "version": "2.6.1"
516 | },
517 | "zendframework/zend-eventmanager": {
518 | "version": "3.2.1"
519 | }
520 | }
521 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | = Symfony application on GKE
2 | :author: Your Name
3 | :email: your@email
4 | :revnumber: 0.3
5 | :revdate: 2021-10-15
6 | :revremark:
7 | :version-label!:
8 | :sectnums:
9 | :toc:
10 | :toclevels: 3
11 | :imagesdir: docs/architecture-images
12 | :source-highlighter: highlightjs
13 | :highlightjsdir: ../github/highlight
14 | // Unfortunately Github doesn't support include statements
15 | ifndef::env-github[]
16 | include::docs/README.config.adoc[]
17 | endif::[]
18 | ifdef::env-github[]
19 | // Copy the included section
20 | :GCP_PROJECT: myproject-123456
21 | :GCP_REGION: us-central1
22 | :GCP_ZONE: us-central1-c
23 | :PROJECT: myproject
24 | :GITHUB_REPO: myproject
25 | :GITHUB_ACCOUNT: myorganization
26 | :WEB_URL: myproject.com
27 | :GKE_CLUSTER: myproject
28 | endif::[]
29 |
30 | == Architecture
31 |
32 | === What is this for?
33 |
34 | This is a recipe for deploying a Symfony application on the Google Kubernetes
35 | Engine. It includes all the configuration files and build scripts.
36 |
37 | It is based on the implementation notes of a recent project. I tidied them up so that
38 | they could be shared. Hope this helps.
39 |
40 | How to use it:
41 |
42 | * Download the repo
43 | * Prepare your local and remote infrastructure as explained on this page
44 | * Install `composer.json`
45 | * Customize `sf/composer.json` if needed and `composer install`
46 | * Adapt the `conf/env` files to your environments
47 | * Adapt the `docs/README.config.adoc` files to your project configuration
48 | * Start developing your Symfony application
49 |
50 | === Principles
51 |
52 | Key principles underlying this design:
53 |
54 | . Adhere to the 12-factors principles
55 | . Identical application code deployed in local and remote environments
56 | . Near-identical infrastructure code deployed in local and remote environments
57 | . Ready to deploy on the GKE (Google Kubernetes Engine)
58 |
59 | === Assumptions
60 |
61 | . Code repository in a private Github repo (the code is easy to adapt if your code sits somewhere else)
62 | . A web reverse proxy has been deployed in the GKE cluster, behind a service of type LoadBalancer (see https://github.com/ericjacolin/apache-proxy-k8s[this recipe] for deploying an Apache+Letsencrypt web server
63 |
64 | === Architecture overview
65 |
66 | ==== Local architecture
67 | image::architecture-local.png[]
68 |
69 | Kubernetes:
70 |
71 | * Kubernetes cluster on Minikube, with Virtualbox VM back-end
72 | * Includes its own Docker environment
73 | * The data volumes on the host are mounted on the `/hosthome` VM location, where they
74 | are visible to the Kubernetes cluster. From there they can be mounted as persistent volumes
75 | onto the container
76 | * Connect to a MySQL database on the host, external to the cluster
77 |
78 | The application manages two types of content files:
79 |
80 | * Public files, served directly by the web proxy server
81 | * Private files, subject to access control, served by the Symfony application
82 | * File storage abstraction using Flysystem, in `local` mode
83 |
84 |
85 | ==== Remote architecture
86 | image::architecture-remote.png[]
87 |
88 | Kubernetes:
89 |
90 | * GKE (Google Kubernetes Engine) Autopilot cluster
91 | * GCP Docker image registry
92 | * Permanent storage on Google Cloud Storage buckets, public buckets for public files,
93 | private buckets for private files
94 | * Connect to Google Cloud SQL/ MySQL database
95 |
96 | Application content files:
97 |
98 | * Public files are served directly by the web proxy server, proxying to Cloud Storage
99 | buckets API (public buckets)
100 | * Private files are served by the web application from private buckets
101 | * File storage abstraction using Flysystem, in `gcloud` (Cloud Storage) mode
102 |
103 | We use Cloud Shell to build and deploy releases, with PHP Deployer scripts:
104 |
105 | * Pull project files from Github repo
106 | * Build Docker images, push images to the GCP Registry
107 | * An init container pulls Symfony files from Github and builds the Symfony application, calling `composer install`
108 | * Update GKE deployment manifest with new image tag
109 |
110 | ==== Identical code
111 |
112 | With this recipe, we have identical Symfony application code and Kubernetes services,
113 | deployments and cronjobs definitions across all environments, local as well as remote.
114 |
115 | All differences between environments are reflected in project-level `.env` configuration files.
116 |
117 | ==== Limitations
118 |
119 | The architecture is suited for a relatively simple web application, with relatively modest
120 | traffic and SLAs, or a MVP.
121 |
122 | The limitations, deliberate for a simple project, can easily be extended as needed as follows.
123 |
124 | Here we assume that an Apache server in reverse proxy mode is deployed on a free tier
125 | Compute Engine VM. For bigger sites one would typically use HTTPS Load Balancers
126 | which are expensive.
127 |
128 | [cols="3*", options="header"]
129 | |===
130 | |Limitation
131 | |Rationale
132 | |How to extend
133 |
134 | |Sessions stored in the container
135 | |Single pod so no need to implement session affinity
136 | |Implement session affinity in the load balancer or reverse proxy +
137 | Alternatively store session information in Google Cloud Memorystore (managed Redis service)
138 |
139 | |Symfony logs stored locally
140 | |Monolog configured to send emails at a certain alert level
141 | |Send Symfony logs to Stackdriver
142 |
143 | |Partial CI/CD
144 | |Deployment by manual execution of a Deployer script in Cloud Shell +
145 | (Github web hooks cannot access Cloud Shell)
146 | |Deploy Jenkins on GCP +
147 | Use Cloud Source Repository instead of Github
148 |
149 | |No test automation in the deployment
150 | |Functional tests are executed locally prior to committing
151 | |Add test tasks to the Deployer script
152 |
153 | |Single pod used for web and batch
154 | |Load on web pod can accommodate batch jobs
155 | |Deploy a separate pod dedicated to batch jobs
156 |
157 | |Symfony Mailer does not yet supports multiple asynchronous transports
158 | |Limitation of the current Mailer version; expecting this to be fixed soon
159 | (https://github.com/symfony/symfony/issues/35750[Issue]) +
160 | For low volumes a single transport suffices
161 | |-
162 | |===
163 |
164 | === Environments
165 |
166 | Environments are defined by two meta-parameters
167 |
168 | * HOST_ENV:
169 | ** `local` (developer's laptop)
170 | ** `remote` (GCP)
171 | * APP_ENV:
172 | ** any name: dev, master, prod, oat, etc.
173 | ** avoid reusing the same name in both a local and a remote environment, since Symfony will use override
174 | configuration files based on the APP_ENV name; those overrides are likely to be different in a local
175 | and a remote deployment
176 |
177 | Symfony configuration `.env` files are named using these two meta-parameters, and named `.env.{APP_ENV}.{HOST_ENV}`, such as `.env.oat.remote`.
178 |
179 | `.env` file contain all environment parameters needed by either Docker, PHP Deployer or Symfony.
180 |
181 | They contain all environment-specific parameters except secrets.
182 |
183 | Symfony looks at OS environment variables when it can't find an environment variable
184 | in the Symfony `.env` file.
185 |
186 | In the local environment, the Symfony working directory is mounted externally on
187 | the container, thus code changes are visible immediately.
188 |
189 | To switch between environments in the local hosting:
190 |
191 | * Copy the relevant `.env` file from `conf/env` to the Symfony root folder `sf`
192 | * Check out the master or dev branch (or feature branch as the case may be)
193 | * (the `.env` file is built by the build process, not committed to source control)
194 |
195 | In the remote (GCP) environments, the build process selects the relevant Symfony `.env` file
196 | and ADD's it to the Docker container.
197 |
198 | === Folder structure
199 |
200 | The project folder structure is as follows:
201 |
202 | [cols="1,2,2", options="header"]
203 | |===
204 | |Folder
205 | |Contents
206 | |Comments
207 |
208 | |``
209 | |Project root, git root
210 | |
211 |
212 | |`{vbar}-- assets`
213 | |Assets to build with Webpack Encore
214 | |css, js
215 |
216 | |`{vbar}-- build`
217 | |Deployment built artefacts (on local deployments)
218 | |Is emptied at the beginning of a build process. Gitignored
219 |
220 | |`{vbar}-- conf`
221 | |Project configuration files
222 | |
223 |
224 | |`{nbsp}{nbsp}{nbsp}{vbar}-- deployer`
225 | |PHP Deployer scripts, Deployer hosts configuration
226 | |
227 |
228 | |`{nbsp}{nbsp}{nbsp}{vbar}-- docker`
229 | |Docker image templates
230 | |Web and batch components
231 |
232 | |`{nbsp}{nbsp}{nbsp}{vbar}-- env`
233 | |Environment variables
234 | |Depend on and
235 |
236 | |`{nbsp}{nbsp}{nbsp}{vbar}-- infra`
237 | |Container configuration file templates
238 | |Apache, PHP, msmtp +
239 | Docker images include the `dockerize` script, which substitutes environment
240 | variables at container build time
241 |
242 | |`{nbsp}{nbsp}{nbsp}{vbar}-- k8s`
243 | |Kubernetes manifest templates: service, deployment, cronjob
244 | |Depend on
245 |
246 | |`{vbar}-- docs`
247 | |Project documentation
248 | |
249 |
250 | |`{vbar}-- sf`
251 | |Symfony project root folder
252 | |The Symfony `.env` file is built dynamically at build time based on dynamically selected `.env..` file
253 |
254 | |`{vbar}-- vendor`
255 | |PHP libraries used by Deployer
256 | |Managed by Composer, distinct from PHP libraries of the Symfony application which are
257 | managed under the `sf` folder
258 | |===
259 |
260 | ==== Deployer
261 |
262 | https://deployer.org/[Deployer] is a simple deployment tool written in PHP.
263 | It is open source and free. It contains pre-defined recipes designed for traditional
264 | FTP deployments; those are not useful in a Kubernetes context, so we wrote new scripts
265 | from scratch.
266 |
267 | We use Deployer scripts to:
268 |
269 | . Generate service/ cronjob manifests (usually done only once)
270 | . Generate deployment manifests (usually done only once)
271 | . Deploy new container version (done at every release, only remotely)
272 |
273 | We run Deployer scripts on:
274 |
275 | * local laptop for local environment
276 | * Cloud Shell for GCP environments
277 |
278 | The scripts take the parameters:
279 |
280 | * `APP_ENV`
281 | * `TAG`:
282 | ** In remote environments, a git tag version is pulled from Github and deployed
283 | ** In local environment: 'current'. The container needs only rebuilding infrequently,
284 | as it mounts the Symfony working directory, obscuring the ADD directive
285 | in the Dockerfile, thus serves whatever is currently checked out
286 | in the working directory. Note that we use 'current' instead of 'latest' as 'latest'
287 | forces a rebuild of the container, which we don't want locally.
288 |
289 | Outline of the remote build process:
290 |
291 | . Execute an initialisation container which:
292 | .. Checks out the tagged version from Github (into a detached branch)
293 | .. Copies relevant Symfony application files from source
294 | .. Copies the relevant `.env..remote` file to both `build/.env` and to `sf/.env`
295 | .. Warms up the Symfony application cache, which is needed by the PHP Opcache directive and must exist
296 | at the time the web container starts
297 | . Build the application container:
298 | .. Passing the `build/.env` as environment parameters
299 | .. ADD the Symfony application and cache files from the init container
300 | .. COPY infrastructure configuration templates
301 | .. RUN `dockerize` on infrastructure configuration templates (see next section)
302 | .. docker push the new container to the GCP container registry
303 | . Build a deployment manifest to `build/deployment.yml`. This manifest contains the new container tag
304 | .. Apply the updated Kubernetes deployment manifest
305 |
306 | Notes:
307 |
308 | * In the web container, the Apache user (www-data) has user:group id 1000:33, whereas in the init container it has user:group id 33:33. This explains the chown commands in the initialisation container
309 | * Another approach would be to use the GCP native Cloud Build service (but this is less portable)
310 | * https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/[SSH key as secret]
311 |
312 | ==== Infrastructure configuration
313 |
314 | The following container infrastructure files are templated. Running `dockerize` interpolates
315 | placeholders in the templates with variables from the Docker build `.env` file.
316 |
317 | [cols="1,2,2", options="header"]
318 | |===
319 | |Template
320 | |Target file in container
321 | |Contents
322 |
323 | |`msmtp.logrotate`
324 | |`/etc/logrotate.d/msmtp`
325 | |msmtp logrotate configuration
326 |
327 | |`msmtprc`
328 | |`/etc/msmtprc`
329 | |msmtp configuration. +
330 | Note that the SMTP password is not stored in clear but obtained from an OS environment
331 | variable.
332 |
333 | |`php.ini`
334 | |`/usr/local/etc/php/php.ini` +
335 | `/etc/php/7.3/apache2/php.ini`
336 | |php.ini for CLI and the Apache PHP module
337 |
338 | |`ssh_config`
339 | |`/.ssh/config`
340 | |Location of the SSH key to the Github private repo. Used by the webinit initialisation container to build
341 | the Symfony application files
342 |
343 | |`virtual-host.conf`
344 | |`/etc/apache2/sites-enabled/virtual-host.conf`
345 | |Single virtual host for the web application
346 | |===
347 |
348 | Notes:
349 |
350 | * For more info on dockerize, https://github.com/powerman/dockerize[see].
351 | * See also `conf/docker/Dockerfile.PHP.example` for a typical Docker RUN command with commonly
352 | used PHP libraries. Adapt as needed.
353 |
354 | ==== Secrets management
355 |
356 | We use two types of secrets:
357 |
358 | * Kubernetes secrets, mounted onto containers:
359 | ** `MAILER_PASSWORD`: SMTP account password
360 | ** `API_KEY`: API key used by the cron service
361 | * Symfony application secrets, packed into a single `SYMFONY_DECRYPTION_SECRET` Kubernetes secret:
362 | ** `APP_SECRET`: encryption key
363 | ** `DB_PASSWORD`: MySQL account password
364 | ** `MAILER_PASSWORD`: SMTP account password
365 |
366 | The `MAILER_PASSWORD` secret, although used by the Symfony application, is needed outside the Symfony environments, to send emails via the batch cron container.
367 |
368 | With this container build process, secrets only exist as container OS environment variables.
369 |
370 | See: https://symfony.com/doc/current/configuration/secrets.html[Managing Symfony secrets]
371 |
372 | == Symfony application
373 |
374 | === Proxies
375 |
376 | In order for the application to correctly reads the headers forwarded by the web reverse proxy.
377 |
378 | .sf/config/packages/framework.yaml
379 | ----
380 | framework:
381 | ...
382 | trusted_proxies: '%env(TRUSTED_PROXIES)%'
383 | trusted_headers: ['x-forwarded-for', 'x-forwarded-host']
384 | ----
385 |
386 | https://symfony.com/doc/current/deployment/proxies.html[See] for reference.
387 |
388 | === Batch jobs
389 |
390 | A Symfony application is typically used in two modes: online requests and batch jobs.
391 |
392 | For this recipe we use a single container to serve both.
393 |
394 | We define batch jobs as Kubernetes cronjobs. Those jobs do the following:
395 |
396 | * Instantiate a simple Alpine/curl container in the cluster
397 | * The container command sends a curl GET request to the application pod inside the cluster
398 | * The request is handled by a normal Symfony controller
399 | * Complete job and log the job status depending on the HTTP response (200 or 500)
400 |
401 | Note that in a traditional, non-containerized Symfony application we would implement
402 | Console Commands in the controller, triggered by command line php calls, scheduled by a cron job.
403 | We can't do this with Kubernetes, since the Kubernetes cronjob cannot execute remote shell commands on the
404 | container and is limited to sending HTTP requests.
405 |
406 | A simpler alternative is to define cron jobs inside your web proxy container, calling application containers using the same API endpoints.
407 |
408 | === Sending emails
409 |
410 | To send emails, we use the following components:
411 |
412 | * The Symfony Mailer library to create emails
413 | * The Symfony Messenger to queue emails in the database
414 | * The msmtp MTA (message transfer agent) to send emails
415 | * Kubernetes cronjobs to process Messenger queues
416 |
417 | The new Mailer library replaces the deprecated SwiftMailer library and is now the recommended
418 | library for new projects.
419 |
420 | For transport the Symfony application does not establish a SMTP connection to the
421 | remote SMTP server, but instead sends messages to a local MTA running in the container.
422 | We use msmtp as MTA. msmtp is a popular successor to sendmail, easier to configure, with a
423 | sendmail-compatible API.
424 | The benefits of using a MTA are:
425 |
426 | * The messages are sent to the remote SMTP server by the MTA background process, not PHP scripts
427 | * The MTA handles exceptions well, such as SMTP server unavailable or returning errors.
428 |
429 | Here we configure two asynchronous transports:
430 |
431 | * `realtime_mailer` for high priority emails (e.g. confirmation after registration)
432 | * `batch_mailer` for low priority emails (e.g. batch newsletter)
433 |
434 | These transports are configured in `Sendmail` mode.
435 |
436 | The process of sending an email is the following:
437 |
438 | * A Symfony controller action (online or batch) generates an email, indicates the transport
439 | method (high or low priority)
440 | * The Messenger component puts these messages in either queue in the database
441 | * The Messenger component processes these two queues in batch mode and sends the emails
442 | to the msmtp MTA (which runs on the same container)
443 | ** The high priority batch job runs every 2mn with a time-out of 100s. If not all queued emails are
444 | processed, the next run starting 20s after will pick them up
445 | ** The low priority batch job runs every 20mn
446 | * The MTA sends the emails to the remote SMTP server roughly in the order it has
447 | received them.
448 |
449 | See previous section for how cronjobs are implemented in Kubernetes.
450 |
451 | In the messenger configuration file we define the queue where to put emails.
452 | Here we use the application database to persist the queues (other methods are available,
453 | notably Redis). We pass the `queue_name` argument to indicate the queue.
454 |
455 | We also define a dead-letter queue where failed messages will be logged. This will be
456 | rare as it is the MTA that is likely to fail while sending emails instead of the Symfony
457 | application.
458 |
459 | .config/packages/messenger.yaml
460 | [source,yaml]
461 | ----
462 | framework:
463 | messenger:
464 | # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
465 | failure_transport: failed
466 |
467 | transports:
468 | # https://symfony.com/doc/current/messenger.html#transport-configuration
469 | failed: 'doctrine://default?queue_name=failed'
470 | # sync: 'sync://'
471 | batch_mailer: 'doctrine://default?queue_name=batch_mailer'
472 | realtime_mailer: 'doctrine://default?queue_name=realtime_mailer'
473 |
474 | routing:
475 | # Route your messages to the transports
476 | 'Symfony\Component\Mailer\Messenger\SendEmailMessage': realtime_mailer
477 | ----
478 |
479 | In the mailer configuration file we define the action to take when actually sending an email.
480 | Usually this is either a SMTP DSN string or Sendmail. Here we use Sendmail. The `native://default` option
481 | uses the `sendmail_path` setting of php.ini, itself defined as `/usr/bin/msmtp -t -v`. See infra configuration
482 | files.
483 |
484 | .config/packages/mailer.yaml
485 | [source,yaml]
486 | ----
487 | framework:
488 | mailer:
489 | transports:
490 | #main: '%env(MAILER_DSN)%'
491 | realtime_mailer: 'native://default'
492 | ----
493 |
494 | Mailer is a new library, and has currently some limitations that we expect to be remediated soon:
495 |
496 | * No support for multiple async transports (https://github.com/symfony/symfony/issues/35750[See])
497 |
498 | The msmtp configuration is defined in the `conf/infra/msmtprc.tpl` template file.
499 | It contains:
500 |
501 | * SMTP account details
502 | * The password is not stored in clear but is defined by a shell command: `"echo $MAILER_PASSWORD"`
503 | * Location of the msmtp log file
504 |
505 | === File storage
506 |
507 | For application user file management we use the Flysystem library, which provides filesystem abstraction
508 | across a number of storage mechanisms.
509 |
510 | * The local environment uses the `local` adapter (local file system)
511 | * The GCP environment uses the `gcloud` adapter (Cloud Storage).
512 |
513 | The application code to read/ write files is identical in all environments. Only the environment configuration
514 | changes. The storage mechanics are abstracted from the application. The application uses `get` and `put`
515 | methods for file paths on a virtual file system.
516 |
517 | Typically the application needs to manage:
518 |
519 | * Public files, served directly by the web proxy without access control
520 | * Private files, subject to access control, and served by the Symfony application
521 | * Each for local and remote file storage
522 |
523 | Hence four adapter configuration:
524 |
525 | .config/packages/flysystem.yaml
526 | [source,yaml]
527 | ----
528 | flysystem:
529 | storages:
530 | storage.private.local:
531 | adapter: 'local'
532 | options:
533 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PRIVATE_BUCKET)%'
534 | storage.public.local:
535 | adapter: 'local'
536 | options:
537 | directory: '%kernel.project_dir%/../local-storage/%env(GCS_PUBLIC_BUCKET)%'
538 | storage.private.gcloud:
539 | adapter: 'gcloud'
540 | options:
541 | client: 'Google\Cloud\Storage\StorageClient' # The service ID of the Google\Cloud\Storage\StorageClient instance
542 | bucket: '%env(GCS_PRIVATE_BUCKET)%'
543 | prefix: ''
544 | api_url: 'https://storage.googleapis.com'
545 | storage.public.gcloud:
546 | adapter: 'gcloud'
547 | options:
548 | client: 'Google\Cloud\Storage\StorageClient' # The service ID of the Google\Cloud\Storage\StorageClient instance
549 | bucket: '%env(GCS_PUBLIC_BUCKET)%'
550 | prefix: ''
551 | api_url: 'https://storage.googleapis.com'
552 | # Aliases based on environment variable
553 | storage.private:
554 | adapter: 'lazy'
555 | options:
556 | source: 'storage.private.%env(STORAGE_ADAPTER)%'
557 | storage.public:
558 | adapter: 'lazy'
559 | options:
560 | source: 'storage.public.%env(STORAGE_ADAPTER)%'
561 | ----
562 |
563 | Thus Symfony creates four "real" services (`flysystem.storage.private.local`, etc)
564 | corresponding to these four adapters.
565 |
566 | We do not use these services but dynamic "alias" (lazy) services `storage.private` and `storage.private`
567 | which depend on the environment.
568 |
569 | To use these services in a controller:
570 |
571 | [source,php]
572 | ----
573 | use League\Flysystem\FilesystemInterface;
574 |
575 | class MyController extends AbstractController
576 | {
577 | /** @var FilesystemInterface $storagePublic Public storage adapter */
578 | private $storagePublic;
579 |
580 | /** @var FilesystemInterface $storagePrivate Private storage adapter */
581 | private $storagePrivate;
582 |
583 | public function __construct(
584 | FilesystemInterface $storagePublic,
585 | FilesystemInterface $storagePrivate
586 | ) {
587 | $this->storagePublic = $storagePublic;
588 | $this->storagePrivate = $storagePrivate;
589 | }
590 |
591 | public function myAction(
592 | Request $request
593 | ) {
594 | $this->storagePrivate->put($file_path, $content);
595 | ----
596 |
597 | On local hosting:
598 |
599 | * External persistent folders on the host are mounted on the Minikube cluster, and
600 | in turn mounted as persistent volumes on the container
601 | * The root of the virtual file system is the mount point of the persistent volume
602 | in the container
603 | * The application read/writes to these folders using the `local` adapter
604 |
605 | On GKE hosting:
606 |
607 | * We use Cloud Storage buckets for storage
608 | * The root of the virtual file system viewed from the application is the bucket
609 | * The application read/writes to these buckets using the `gcloud` adapter, which uses API calls
610 | to Cloud Storage
611 | * The buckets must be configured for ACL access as the Symfony application will use
612 | a GCP service account to access the buckets.
613 |
614 | The `CDN_URL` environment variable is used by the application to create URLs to public
615 | assets that are served directly by the web proxy, outside the application.
616 |
617 | == Local environment
618 |
619 | In the local environment we instantiate a Kubernetes engine using the Minikube
620 | package.
621 |
622 | === Minikube
623 |
624 | . Install kubectl
625 | . Install https://kubernetes.io/docs/setup/learning-environment/minikube[minikube]
626 | via direct download
627 | . Bind mount the host folders used to persist application user data host folder to `/home`,
628 | which is accessible from within the Minikube cluster as `/hosthome`
629 |
630 | Increase the default CPU allocated to the cluster VM (2CPU) to 4CPU:
631 |
632 | ----
633 | minikube delete
634 | minikube config set cpus 4
635 | minikube start
636 | ----
637 |
638 | .In the local shell:
639 | [source,bash,subs=attributes+]
640 | ----
641 | sudo mount --bind /opt/data/storage-buckets /home/storage-buckets \
642 | && sudo mount --bind /opt/data/projects/myproject /home/myproject \
643 | \
644 | && minikube start --driver=virtualbox --cpus 4 \
645 | && minikube tunnel
646 | ----
647 |
648 | Then optionally open the minikube dashboard in a browser:
649 |
650 | .In the local shell:
651 | [source,bash,subs=attributes+]
652 | ----
653 | # Open the minikube dashboard in a browser
654 | minikube dashboard
655 | ----
656 |
657 | The dashboard is very handy.
658 |
659 | On Linux, from within the minikube cluster, the host does not have a DNS name. This is available on MacOS
660 | hosts (name is `host.docker.internal`). There is a https://github.com/moby/moby/pull/40007[pull request]
661 | to this effect.
662 |
663 | You have to use the host IP address `10.0.2.2` instead.
664 |
665 | === Docker context
666 |
667 | Before building containers (`docker build`) ensure you are in the correct context. Minikube has its
668 | own Docker engine running inside its Virtualbox VM, distinct from that of the laptop host.
669 |
670 | .In the local shell:
671 | [source,bash]
672 | ----
673 | # switch to the Minikube VM context (must be run in each new terminal session)
674 | eval $(minikube docker-env)
675 | # switch back to local Docker context
676 | eval $(minikube docker-env -u)
677 | ----
678 |
679 | Notes:
680 |
681 | * I tried to use Minikube with `--vm-driver=none`, so that it would use the host Docker
682 | engine, but it didn't work and probably never will
683 | * The Minikube cluster node is visible in the host at `http://192.168.99.100:31645/`
684 | (the port is randomly assigned at minikube startup).
685 |
686 | === Kubernetes context
687 |
688 | You will be switching between the local and GKE Kubernetes contexts. Ensure you are in the correct
689 | context before firing `kubectl apply` commands.
690 |
691 | .In the local shell:
692 | [source,bash,subs=attributes+]
693 | ----
694 | kubectl config get-contexts
695 | # output:
696 | CURRENT NAME CLUSTER
697 | * gke_{GCP_PROJECT}_{GCP_REGION}_{GKE_CLUSTER} gke_{GCP_PROJECT}_{GCP_REGION}_{GKE_CLUSTER}
698 | minikube minikube
699 | # To switch to another context:
700 | kubectl config use-context gke_{GCP_PROJECT}_{GCP_REGION}_{GKE_CLUSTER}
701 | # or
702 | kubectl config use-context minikube
703 | ----
704 |
705 | == GCP environment
706 |
707 | === GCP project
708 |
709 | Set project defaults:
710 |
711 | .In the local shell:
712 | [source,bash,subs=attributes+]
713 | ----
714 | gcloud config set project {GCP_PROJECT}
715 | gcloud config set compute/region {GCP_REGION}
716 | gcloud config set compute/zone {GCP_ZONE}
717 | ----
718 |
719 | Generate SSH keys, store them locally in `~/.ssh/`.
720 |
721 | Copy the keys in your Cloud Shell `~/.ssh/` folder.
722 |
723 | === Cloud Shell
724 |
725 | We use Cloud Shell as a build and deployment environment
726 |
727 | .In the Cloud shell:
728 | [source,bash,subs=attributes+]
729 | ----
730 | # set project
731 | gcloud config set project {GCP_PROJECT}
732 | # clone the project repo:
733 | git clone git@github.com:{GITHUB_ACCOUNT}/{GITHUB_REPO}.git
734 | # install the Deployer vendor libraries
735 | cd {PROJECT} && composer install
736 | ----
737 |
738 | To avoid having to reinstall Deployer at every new session, add the following lines to your Cloud Shell
739 | customize_environment file:
740 |
741 | .~/customize_environment
742 | ----
743 | #!/bin/sh
744 | curl -LO https://deployer.org/deployer.phar
745 | sudo mv deployer.phar /usr/local/bin/dep
746 | sudo chmod +x /usr/local/bin/dep
747 | ----
748 |
749 | It is useful to SCP to Cloud Shell. Note that paths must be absolute. Use the `--recurse` flag
750 | for recursive copying.
751 |
752 | .In the local shell:
753 | [source,bash]
754 | ----
755 | # To copy a remote directory to your local machine:
756 | gcloud alpha cloud-shell scp \
757 | cloudshell:~/REMOTE-DIR \
758 | localhost:~/LOCAL-DIR
759 | # Conversely:
760 | gcloud alpha cloud-shell scp \
761 | localhost:~/LOCAL-DIR \
762 | cloudshell:~/REMOTE-DIR
763 | ----
764 |
765 | ==== Install PHP 7.3
766 |
767 | https://computingforgeeks.com/how-to-install-php-7-3-on-ubuntu-18-04-ubuntu-16-04-debian/[See]
768 |
769 | .In the Google Cloud shell:
770 | [source,bash]
771 | ----
772 | sudo add-apt-repository ppa:ondrej/php
773 | sudo apt-get update \
774 | sudo apt install php7.3 php7.3-cli php7.3-mbstring php7.3-curl php7.3-xml php7.3-zip php7.3-curl
775 | sudo update-alternatives --set php /usr/bin/php7.3
776 | ----
777 |
778 | TO DO: replace the PHP CLI install by a Docker container... nicer and cleaner
779 |
780 | ==== Github SSH key
781 |
782 | https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent[See]
783 |
784 | Create a new SSH key labelled "{PROJECT}-vm" locally. Register it on Github.
785 | Also create a corresponding config file:
786 |
787 | .In the local ~/.ssh/config file, append:
788 | [source,bash,subs=attributes+]
789 | ----
790 | Host github.com
791 | User git
792 | Hostname github.com
793 | PreferredAuthentications publickey
794 | IdentityFile ~/.ssh/{PROJECT}-vm_rsa
795 | ----
796 |
797 | ==== Composer
798 |
799 | Composer is used to manage dependencies for PHP Deployer.
800 |
801 | .In the Google Cloud shell:
802 | [source,bash,subs=attributes+]
803 | ----
804 | # PHP Deployer
805 | cd ~/{PROJECT}
806 | composer install
807 | # Symfony
808 | cd sf
809 | composer install
810 | ----
811 |
812 | === Cloud storage
813 |
814 | It is a good practice to create bucket names that are domain names associated to your
815 | project, as it guarantees global unicity.
816 |
817 | This requires domain ownership verification:
818 |
819 | * In the Google Search Console, add Property: `{WEB_URL}`
820 | * In your domain name DNS manager, add a TXT record with the provided text.
821 |
822 | Create the following buckets:
823 |
824 | * `app.{WEB_URL}`: private data, live site
825 | * `app-test.{WEB_URL}`: private data, test site
826 | * `cdn.{WEB_URL}`: public data, live site
827 | * `cdn-test.{WEB_URL}`: public data, test site
828 |
829 | Set bucket access control policy to ACLs, since the PHP flysystem API will use a service
830 | account to access the Cloud Storage API.
831 |
832 | To make buckets public:
833 |
834 | .In the local shell:
835 | [source,bash,subs=attributes+]
836 | ----
837 | gsutil iam ch allUsers:objectViewer gs://cdn-test.{WEB_URL}
838 | gsutil iam ch allUsers:objectViewer gs://cdn.{WEB_URL}
839 | ----
840 |
841 | Using Chrome, upload folders/ files using the Cloud Storage console or the `gsutil cp` command.
842 |
843 | Example of commands to copy files at the command line from local and remote environments:
844 |
845 | .In the local shell:
846 | [source,bash,subs=attributes+]
847 | ----
848 | # local to remote
849 | gsutil cp * gs://cdn.{WEB_URL}/dir
850 | # remote to local
851 | gsutil cp gs://cdn.{WEB_URL}/dir/* .
852 | # remote to remote
853 | gsutil cp gs://cdn.{WEB_URL}/dir/* gs://cdn-test.{WEB_URL}.org/dir
854 | ----
855 |
856 | === Cloud database
857 |
858 | ==== Create the database
859 |
860 | On the Gcloud SQL Console, create DB instance called `{PROJECT}-db` (MySQL 5.7):
861 |
862 | Create Database:
863 |
864 | * Character set/ Collation => `utf8mb4/ utf8mb4_unicode_ci`
865 | * Connectivity: Private IP
866 |
867 | Don't use the recommended collation for MySQL 8.0 `utf8mb4_0900_ai_ci` as it is
868 | not supported on MySQL 5.7.
869 |
870 | Note down the DB instance external IP address (`10.1.2.3`). You will use it in your
871 | `.env` configuration files.
872 |
873 | ==== Test connectivity from VPC
874 |
875 | Instantiate temporarily a Compute Engine VM in the same VPC.
876 |
877 | Install the mysql CLI: `apt-get update && apt-get install -y mysql-client-5.7`
878 |
879 | .In the VM shell:
880 | [source,bash,subs=attributes+]
881 | ----
882 | # root user - enter password at prompt
883 | mysql -u root -p -h 10.1.2.3
884 | # application user - enter password at prompt
885 | mysql -u {PROJECT}_dev -p -h 10.1.2.3
886 | ----
887 |
888 | Note:
889 |
890 | * You can't connect from the Cloud Shell, as it is outside the project VPC.
891 |
892 | ==== Create DB accounts
893 |
894 | On the Gcloud SQL Console > Users > Create MySQL user accounts:
895 |
896 | * `{PROJECT}_dev`
897 | * `{PROJECT}_master`
898 |
899 | Allow from any host (%)
900 |
901 | Apply GRANT commands as required by your application. A typical one would be:
902 |
903 | .In the MySQL shell:
904 | [source,mysql]
905 | ----
906 | GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, EXECUTE,
907 | CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER, LOCK TABLES
908 | ON ``.* TO ''@'%';
909 | FLUSH PRIVILEGES;
910 | ----
911 |
912 | ==== Application service account
913 |
914 | Create a `{PROJECT}-dev` Cloud IAM service account for the application. Add roles:
915 |
916 | * Storage > Storage Object Admin
917 | * Cloud SQL > Cloud SQL Client
918 |
919 | The service account is named `{PROJECT}-dev@{GCP_PROJECT}.iam.gserviceaccount.com`
920 |
921 | Create a JSON key associated to this account => `{GCP_PROJECT}-abcdef123456.json`.
922 |
923 | ==== Import a database
924 |
925 | To import a database, instantiate a temporary VM.
926 |
927 | All tables must be in the InnoDB format.
928 |
929 | . Export the DB in SQL format with Adminer or PhpMyAdmin
930 | . SCP the SQL file to the VM:
931 |
932 | .In the local shell:
933 | [source,bash,subs=attributes+]
934 | ----
935 | gcloud compute scp \
936 | ~/{PROJECT}_dev.sql \
937 | temp-vm:/tmp
938 | ----
939 |
940 | [start=3]
941 | . Import the DB:
942 |
943 | .In the temporary VM shell:
944 | [source,bash,subs=attributes+]
945 | ----
946 | mysql -u root -p -h 10.1.2.3 {PROJECT}_dev < /tmp/{PROJECT}_dev.sql
947 | ----
948 |
949 | ==== Artifact Registry
950 |
951 | The Container Registry is deprecated. Use the Artifact Registry instead.
952 |
953 | * In the Console > Artifact Registry, create a repo named `{GCP_PROJECT}`
954 | * In Cloud Shell run `gcloud auth configure-docker {GCP_REGION}-docker.pkg.dev`
955 | * Assign role "Artifact Registry Writer" to your service account
956 |
957 | === GKE
958 |
959 | ==== Create the cluster
960 |
961 | Create an Autopilot cluster. Workload identity is enabled by default.
962 |
963 | ==== Bind the service account
964 |
965 | Creating the cluster creates automatically:
966 |
967 | * A `default` namespace
968 | * A Kubernetes service account (`kubectl get serviceaccount --namespace default`).
969 | At this stage this service account controls access only within the cluster.
970 |
971 | The Kubernetes service account needs to access Google resources, so we bind it to the
972 | application Google service account previously created.
973 |
974 | .In the local shell:
975 | [source,bash,subs=attributes+]
976 | ----
977 | gcloud iam service-accounts add-iam-policy-binding \
978 | --role roles/iam.workloadIdentityUser \
979 | --member "serviceAccount:{GCP_PROJECT}.svc.id.goog[default/default]" \
980 | {GCP_PROJECT}-dev@{GCP_PROJECT}.iam.gserviceaccount.com
981 | ----
982 |
983 | Add corresponding annotation to the Kubernetes service account:
984 |
985 | .In the local shell:
986 | [source,bash,subs=attributes+]
987 | ----
988 | kubectl annotate serviceaccount \
989 | --namespace default \
990 | default \
991 | iam.gke.io/gcp-service-account={GCP_PROJECT}-dev@{GCP_PROJECT}.iam.gserviceaccount.com
992 | ----
993 |
994 | Verify that the service account is configured correctly by running a test container
995 | provided by Google:
996 |
997 | .In the local shell:
998 | [source,bash,subs=attributes+]
999 | ----
1000 | kubectl run -it \
1001 | --generator=run-pod/v1 \
1002 | --image google/cloud-sdk \
1003 | --serviceaccount default \
1004 | --namespace default \
1005 | workload-identity-test
1006 | ----
1007 |
1008 | .In the container shell:
1009 | [source,bash]
1010 | ----
1011 | gcloud auth list
1012 | ----
1013 | This should display a single Google service account, the one bind'ed earlier. This is the service account
1014 | the pod will use to access GCP services.
1015 |
1016 | Once done, delete the `workload-identity-test` pod.
1017 |
1018 | == Deployment
1019 |
1020 | === Deploy locally
1021 |
1022 | ==== Secrets
1023 |
1024 | Create your secrets for each environment in a safe location outside the project directory.
1025 |
1026 | Naming convention: `secrets...yml`
1027 | Those are in the form:
1028 |
1029 | ./secrets/secrets.local.dev.yml
1030 | [source,yaml,subs=attributes+]
1031 | ----
1032 | apiVersion: v1
1033 | kind: Secret
1034 | metadata:
1035 | name: {PROJECT}-{APP_ENV}-sf-secrets
1036 | type: Opaque
1037 | data:
1038 | # php -r 'echo base64_encode(base64_encode(require "config/secrets/dev/dev.decrypt.private.php"));'
1039 | SYMFONY_DECRYPTION_SECRET: YWJjZGVmZ2h1aWRVQUlQR0RBWkVQVUlJVUdQ
1040 | # echo -n 'secret1' | base64
1041 | API_KEY: c2VjcmV0MQ==
1042 | # echo -n 'secret2' | base64
1043 | MAILER_PASSWORD: c2VjcmV0Mg==
1044 | ----
1045 |
1046 | Deploy Kubernetes secrets locally:
1047 |
1048 | .In the local shell:
1049 | [source,bash,subs=attributes+]
1050 | ----
1051 | cd dir
1052 | kubectl config use-context minikube
1053 | kubectl apply -f secrets.local.dev.yml
1054 | kubectl apply -f secrets.local.master.yml
1055 | ----
1056 |
1057 | ==== Cronjobs
1058 |
1059 | To execute cron jobs we instantiate a very simple Docker Alpine image with the cUrl
1060 | library.
1061 |
1062 | .In the local shell:
1063 | [source,bash,subs=attributes+]
1064 | ----
1065 | cd {PROJECT}
1066 | # Set Docker and Kubernetes contexts to Minikube
1067 | eval $(minikube docker-env)
1068 | # Build the Docker cronjob image
1069 | docker build -f build/Dockerfile.cronjob -t k8s-cronjob:current .
1070 | ----
1071 |
1072 | ==== Services
1073 |
1074 | We build services manifests with PHP Deployer and deploy them using kubectl.
1075 | This is usually done only once.
1076 |
1077 | .In the local shell:
1078 | [source,bash,subs=attributes+]
1079 | ----
1080 | cd {PROJECT}
1081 | # Set Docker and Kubernetes contexts to Minikube
1082 | kubectl config use-context minikube
1083 | # Deploy services (dev environment)
1084 | php vendor/bin/dep --file=conf/deployer/deploy.php \
1085 | --hosts=localhost gen-service -o APP_ENV=dev
1086 | kubectl apply -f build/service.yml
1087 | kubectl apply -f build/cronjob.local.yml
1088 | # Deploy services (master environment)
1089 | php vendor/bin/dep --file=conf/deployer/deploy.php \
1090 | --hosts=localhost gen-service -o APP_ENV=master
1091 | kubectl apply -f build/service.yml
1092 | kubectl apply -f build/cronjob.local.yml
1093 | ----
1094 |
1095 | ==== Releases
1096 |
1097 | The web application container mounts the Symfony working directory. There is no need
1098 | to rebuild the container, unless, say, you need to add a PHP library. Just switch git
1099 | branches as needed.
1100 |
1101 | The Minikube web application is visible on the host at: `192.168.99.100:32745`
1102 | (adapt the port number)
1103 |
1104 | === Deploy on GKE
1105 |
1106 | We use Cloud Shell to build and deploy on GKE.
1107 |
1108 | ==== Pull the repo
1109 |
1110 | .In the Cloud shell:
1111 | [source,bash,subs=attributes+]
1112 | ----
1113 | git clone git@github.com:{GITHUB_ACCOUNT}/{GITHUB_REPO}.git
1114 | cd {PROJECT}
1115 | ----
1116 |
1117 | ==== Secrets
1118 |
1119 | Deploy Kubernetes secrets. Here those are the same as for the local environment.
1120 |
1121 | .In the local shell:
1122 | [source,bash,subs=attributes+]
1123 | ----
1124 | cd your-secrets-dir
1125 | # Switch to GKE context
1126 | kubectl config use-context gke_{GCP_PROJECT}_{GCP_REGION}_{GKE_CLUSTER}
1127 | kubectl apply -f secrets.oat.remote.yml
1128 | kubectl apply -f secrets.prod.remote.yml
1129 | ----
1130 |
1131 | ==== Cronjobs
1132 |
1133 | Push the Alpine/cUrl image to GCR:
1134 |
1135 | .In the Cloud shell:
1136 | [source,bash,subs=attributes+]
1137 | ----
1138 | cd {PROJECT}
1139 | docker build -f conf/docker/Dockerfile.cronjob -t k8s-cronjob:current .
1140 | docker tag k8s-cronjob:current gcr.io/{GCP_PROJECT}/k8s-cronjob:current
1141 | docker push gcr.io/{GCP_PROJECT}/k8s-cronjob:current
1142 | ----
1143 |
1144 | ==== Services
1145 |
1146 | Deploy services manifests:
1147 |
1148 | .In the Cloud shell:
1149 | [source,bash,subs=attributes+]
1150 | ----
1151 | cd {PROJECT}
1152 | # Switch to GKE context
1153 | kubectl config use-context gke_{GCP_PROJECT}_{GCP_REGION}_{GKE_CLUSTER}
1154 | # Deploy services (OAT on test.)
1155 | php vendor/bin/dep --file=conf/deployer/deploy.php \
1156 | --hosts=remote gen-service -o APP_ENV=oat
1157 | kubectl apply -f build/service.yml
1158 | kubectl apply -f build/cronjob.remote.yml
1159 | # Deploy services (Prod on www.)
1160 | php vendor/bin/dep --file=conf/deployer/deploy.php \
1161 | --hosts=remote gen-service -o APP_ENV=prod
1162 | kubectl apply -f build/service.yml
1163 | kubectl apply -f build/cronjob.remote.yml
1164 | ----
1165 |
1166 | ==== Releases
1167 |
1168 | The release process is as follows:
1169 |
1170 | . Git tag a release version
1171 | . Push the tag to Github
1172 | . In the Cloud Shell git pull
1173 | . Execute the Deployer script; this:
1174 | .. Builds a new container with a Docker tag identical to the git tag
1175 | .. Applies an updated deployment manifest with the new Docker tag
1176 |
1177 | Commands:
1178 |
1179 | .In the local shell:
1180 | [source,bash,subs=attributes+]
1181 | ----
1182 | # tag a commit locally
1183 | git tag 0.4
1184 | # push tags
1185 | git push origin --tags
1186 | ----
1187 |
1188 | .In the Cloud shell:
1189 | [source,bash,subs=attributes+]
1190 | ----
1191 | # Deploy new version - Test
1192 | cd {PROJECT}
1193 | php vendor/bin/dep --file=conf/deployer/deploy.php --hosts=remote deploy-remote \
1194 | -o APP_ENV=dev -o TAG=0.4
1195 | # Deploy new version - Prod
1196 | cd {PROJECT}
1197 | php vendor/bin/dep --file=conf/deployer/deploy.php --hosts=remote deploy-remote \
1198 | -o APP_ENV=master -o TAG=0.4
1199 | ----
1200 |
1201 | == References
1202 |
1203 | Some random resources that I found useful. Not all apply to this recipe.
1204 |
1205 | === Docker
1206 |
1207 | http://docs.blowb.org/index.html[The Blowb Project - Deploy Integrated Apps Using Docker]
1208 | (Not used in this article but looks like it has many good ideas)
1209 |
1210 | === Kubernetes
1211 |
1212 | https://codeburst.io/getting-started-with-kubernetes-deploy-a-docker-container-with-kubernetes-in-5-minutes-eb4be0e96370
1213 |
1214 | https://www.mirantis.com/blog/introduction-to-yaml-creating-a-kubernetes-deployment/
1215 |
1216 | https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#container-v1-core
1217 |
1218 | https://vsupalov.com/yaml-kubernetes-examples-docs-spec/
1219 |
1220 | https://dzone.com/articles/kubernetes-cron-jobs
1221 |
1222 | https://stackoverflow.com/questions/14155596/how-to-substitute-shell-variables-in-complex-text-files
1223 |
1224 | https://dzone.com/articles/how-i-switched-my-blog-from-ovh-to-google-containe
1225 |
1226 | https://gravitational.com/blog/troubleshooting-kubernetes-networking
1227 |
1228 | https://estl.tech/configuring-https-to-a-web-service-on-google-kubernetes-engine-2d71849520d
1229 |
1230 | https://blog.container-solutions.com/kubernetes-deployment-strategies
1231 |
1232 | https://medium.com/google-cloud/kubernetes-best-practices-8d5cd03446e2
1233 |
1234 | https://stackoverflow.com/questions/22944631/how-to-get-the-ip-address-of-the-docker-host-from-inside-a-docker-container
1235 |
1236 | === GKE
1237 |
1238 | https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity
1239 |
1240 | https://cloud.google.com/solutions/using-gcp-services-from-gke
1241 |
1242 | https://medium.com/redpoint/cost-effective-kubernetes-on-google-cloud-61067185ebe8
1243 |
1244 | https://rominirani.com/google-cloud-platform-factors-to-control-your-costs-5a256ed207f1?gi=e90cc7e943c8
1245 |
1246 | https://medium.com/google-cloud/kubernetes-day-one-30a80b5dcb29
1247 |
1248 | === Symfony
1249 |
1250 | https://medium.com/@galopintitouan/how-to-build-a-scalable-symfony-application-on-kubernetes-30f23bf304e
1251 |
1252 | https://titouangalopin.com/introducing-the-official-flysystem-bundle/
1253 |
1254 | https://itnext.io/scaling-your-symfony-application-and-preparing-it-for-deployment-on-kubernetes-c102bf246a93
1255 |
1256 | https://medium.com/@joeymasip/how-to-create-an-api-with-symfony-4-and-jwt-b2334a8fbec2
1257 |
1258 | https://www.jakelitwicki.com/2015/05/26/a-standard-gitignore-for-symfony-applications/
1259 |
1260 | === Mailer
1261 |
1262 | https://backbeat.tech/blog/sending-emails-with-symfony/
1263 |
1264 | https://symfony.com/doc/4.4/mailer.html
1265 |
1266 | https://github.com/cmaessen/docker-php-sendmail/blob/master/Dockerfile
1267 |
--------------------------------------------------------------------------------