pulumi-django-azure 1.0.21__tar.gz → 1.0.23__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pulumi-django-azure might be problematic. Click here for more details.

Files changed (19) hide show
  1. {pulumi_django_azure-1.0.21/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.23}/PKG-INFO +61 -97
  2. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/README.md +44 -91
  3. pulumi_django_azure-1.0.23/pyproject.toml +82 -0
  4. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/src/pulumi_django_azure/__init__.py +1 -0
  5. pulumi_django_azure-1.0.23/src/pulumi_django_azure/azure_helper.py +67 -0
  6. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/src/pulumi_django_azure/django_deployment.py +20 -21
  7. pulumi_django_azure-1.0.23/src/pulumi_django_azure/management/commands/__init__.py +0 -0
  8. pulumi_django_azure-1.0.23/src/pulumi_django_azure/management/commands/purge_cdn.py +52 -0
  9. pulumi_django_azure-1.0.23/src/pulumi_django_azure/middleware.py +43 -0
  10. pulumi_django_azure-1.0.23/src/pulumi_django_azure/settings.py +154 -0
  11. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23/src/pulumi_django_azure.egg-info}/PKG-INFO +61 -97
  12. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/src/pulumi_django_azure.egg-info/SOURCES.txt +6 -1
  13. pulumi_django_azure-1.0.23/src/pulumi_django_azure.egg-info/requires.txt +13 -0
  14. pulumi_django_azure-1.0.21/pyproject.toml +0 -62
  15. pulumi_django_azure-1.0.21/src/pulumi_django_azure.egg-info/requires.txt +0 -3
  16. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/LICENSE +0 -0
  17. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/setup.cfg +0 -0
  18. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/src/pulumi_django_azure.egg-info/dependency_links.txt +0 -0
  19. {pulumi_django_azure-1.0.21 → pulumi_django_azure-1.0.23}/src/pulumi_django_azure.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.21
3
+ Version: 1.0.23
4
4
  Summary: Simply deployment of Django on Azure with Pulumi
5
5
  Author-email: Maarten Ureel <maarten@youreal.eu>
6
6
  License: MIT License
@@ -30,12 +30,23 @@ Keywords: django,pulumi,azure
30
30
  Classifier: License :: OSI Approved :: MIT License
31
31
  Classifier: Programming Language :: Python
32
32
  Classifier: Programming Language :: Python :: 3
33
- Requires-Python: >=3.9
33
+ Requires-Python: <3.14,>=3.11
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: pulumi>=3.146.0
37
- Requires-Dist: pulumi-azure-native>=2.82.0
38
- Requires-Dist: pulumi-random>=4.17.0
36
+ Requires-Dist: azure-identity<2.0.0,>=1.21.0
37
+ Requires-Dist: azure-keyvault-secrets<5.0.0,>=4.9.0
38
+ Requires-Dist: azure-mgmt-cdn<14.0.0,>=13.1.1
39
+ Requires-Dist: azure-mgmt-resource<24.0.0,>=23.3.0
40
+ Requires-Dist: django<6.0.0,>=5.1.7
41
+ Requires-Dist: django-azure-communication-email<2.0.0,>=1.3.0
42
+ Requires-Dist: django-environ<0.13.0,>=0.12.0
43
+ Requires-Dist: django-redis<6.0.0,>=5.4.0
44
+ Requires-Dist: django-storages[azure]<2.0.0,>=1.14.5
45
+ Requires-Dist: pulumi>=3.156.0
46
+ Requires-Dist: pulumi-azure-native>=2.89.1
47
+ Requires-Dist: pulumi-random>=4.18.0
48
+ Requires-Dist: redis[hiredis]<6.0.0,>=5.2.1
49
+ Dynamic: license-file
39
50
 
40
51
  # Pulumi Django Deployment
41
52
 
@@ -57,12 +68,36 @@ Your Django project should contain a folder `cicd` with these files:
57
68
  Note that this runs in the identity of the build container, so you should not run database or storage manipulations here.
58
69
  * startup.sh: commands to run the actual application. I recommend to put at least:
59
70
  ```bash
71
+ # Compile translations
72
+ apt-get -y install gettext
73
+ python manage.py compilemessages
74
+
75
+ # Run collectstatic in the background
76
+ sh cicd/collectstatic.sh &
77
+
60
78
  python manage.py migrate
61
- python manage.py collectstatic --noinput
62
- python manage.py purge_cdn
63
79
  gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
64
80
  ```
65
- Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
81
+
82
+ Be sure to change `yourapplication` in the above.
83
+
84
+ * collecstatic.sh
85
+ ```bash
86
+ #!/bin/bash
87
+
88
+ LOGFILE="/var/log/django_collectstatic.log"
89
+
90
+ # Collectstatic
91
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting collectstatic" >> $LOGFILE
92
+ python manage.py collectstatic --noinput -v 2 >> $LOGFILE
93
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished collectstatic" >> $LOGFILE
94
+
95
+ # Purge the CDN
96
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting purge_cdn" >> $LOGFILE
97
+ python manage.py purge_cdn >> $LOGFILE
98
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished purge_cdn" >> $LOGFILE
99
+ ```
100
+
66
101
  ## Installation
67
102
  This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
68
103
 
@@ -127,8 +162,23 @@ django.add_database_administrator(
127
162
  )
128
163
  ```
129
164
 
130
- ## Deployment steps
165
+ ## Changes to your Django project
166
+ 1. Add `pulumi_django_azure` to your `INSTALLED_APPS`
167
+ 2. Add to your settings file:
168
+ ```python
169
+ from pulumi_django_azure.settings import * # noqa: F403
170
+
171
+ # This will provide the management command to purge the CDN
172
+ INSTALLED_APPS += ["pulumi_django_azure"]
173
+
174
+ # This will provide the health check middleware that will also take care of credential rotation.
175
+ MIDDLEWARE += ["pulumi_django_azure.middleware.HealthCheckMiddleware"]
176
+ ```
177
+ This will pre-configure most settings to make your app work on Azure. You can check the source for details,
178
+ and ofcourse override any value after importing them.
179
+
131
180
 
181
+ ## Deployment steps
132
182
  1. Deploy without custom hosts (for CDN and websites)
133
183
  2. Configure the PostgreSQL server (create and grant permissions to role for your websites)
134
184
  3. Retrieve the deployment SSH key and configure your remote GIT repository with it
@@ -211,19 +261,8 @@ The environment variable will be suffixed with `_SECRET_NAME`.
211
261
 
212
262
  Then, in your application, retrieve this data from the vault, e.g.:
213
263
  ```python
214
- from azure.keyvault.secrets import SecretClient
215
- from azure.identity import DefaultAzureCredential
216
-
217
- # Azure credentials
218
- azure_credential = DefaultAzureCredential()
219
-
220
- # Azure Key Vault
221
- AZURE_KEY_VAULT = env("AZURE_KEY_VAULT")
222
- AZURE_KEY_VAULT_URI = f"https://{AZURE_KEY_VAULT}.vault.azure.net"
223
- azure_key_vault_client = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=azure_credential)
224
-
225
264
  # Social Auth settings
226
- oauth_secret = azure_key_vault_client.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
265
+ oauth_secret = AZURE_KEY_VAULT_CLIENT.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
227
266
  oauth_secret = json.loads(oauth_secret.value)
228
267
  SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"]
229
268
  SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"]
@@ -257,81 +296,6 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
257
296
 
258
297
  This would then trigger a redeploy everytime you make a commit to your live branch.
259
298
 
260
- ## CDN Purging
261
- We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
262
- ```python
263
- import os
264
-
265
- from azure.mgmt.cdn import CdnManagementClient
266
- from azure.mgmt.cdn.models import PurgeParameters
267
- from django.core.management.base import BaseCommand
268
-
269
- from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
270
-
271
-
272
- class Command(BaseCommand):
273
- help = "Purges the CDN endpoint"
274
-
275
- def add_arguments(self, parser):
276
- parser.add_argument(
277
- "--wait",
278
- action="store_true",
279
- help="Wait for the purge operation to complete",
280
- )
281
-
282
- def handle(self, *args, **options):
283
- # Read environment variables
284
- resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
285
- profile_name = os.getenv("CDN_PROFILE")
286
- endpoint_name = os.getenv("CDN_ENDPOINT")
287
- content_paths = ["/*"]
288
-
289
- # Ensure all required environment variables are set
290
- if not all([resource_group, profile_name, endpoint_name]):
291
- self.stderr.write(self.style.ERROR("Missing required environment variables."))
292
- return
293
-
294
- # Authenticate with Azure
295
- cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
296
-
297
- try:
298
- # Purge the CDN endpoint
299
- purge_operation = cdn_client.endpoints.begin_purge_content(
300
- resource_group_name=resource_group,
301
- profile_name=profile_name,
302
- endpoint_name=endpoint_name,
303
- content_file_paths=PurgeParameters(content_paths=content_paths),
304
- )
305
-
306
- # Check if the --wait argument was provided
307
- if options["wait"]:
308
- purge_operation.result() # Wait for the operation to complete
309
- self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
310
- else:
311
- self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
312
-
313
- except Exception as e:
314
- self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
315
- ```
316
-
317
- And our azure_helper:
318
- ```python
319
- from azure.identity import DefaultAzureCredential
320
- from azure.mgmt.resource import SubscriptionClient
321
-
322
- # Azure credentials
323
- AZURE_CREDENTIAL = DefaultAzureCredential()
324
-
325
-
326
- def get_db_password() -> str:
327
- return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
328
-
329
-
330
- def get_subscription_id() -> str:
331
- subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
332
- subscriptions = list(subscription_client.subscriptions.list())
333
- return subscriptions[0].subscription_id
334
- ```
335
299
 
336
300
  ## Change requests
337
301
  I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
@@ -18,12 +18,36 @@ Your Django project should contain a folder `cicd` with these files:
18
18
  Note that this runs in the identity of the build container, so you should not run database or storage manipulations here.
19
19
  * startup.sh: commands to run the actual application. I recommend to put at least:
20
20
  ```bash
21
+ # Compile translations
22
+ apt-get -y install gettext
23
+ python manage.py compilemessages
24
+
25
+ # Run collectstatic in the background
26
+ sh cicd/collectstatic.sh &
27
+
21
28
  python manage.py migrate
22
- python manage.py collectstatic --noinput
23
- python manage.py purge_cdn
24
29
  gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
25
30
  ```
26
- Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
31
+
32
+ Be sure to change `yourapplication` in the above.
33
+
34
+ * collecstatic.sh
35
+ ```bash
36
+ #!/bin/bash
37
+
38
+ LOGFILE="/var/log/django_collectstatic.log"
39
+
40
+ # Collectstatic
41
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting collectstatic" >> $LOGFILE
42
+ python manage.py collectstatic --noinput -v 2 >> $LOGFILE
43
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished collectstatic" >> $LOGFILE
44
+
45
+ # Purge the CDN
46
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting purge_cdn" >> $LOGFILE
47
+ python manage.py purge_cdn >> $LOGFILE
48
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished purge_cdn" >> $LOGFILE
49
+ ```
50
+
27
51
  ## Installation
28
52
  This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
29
53
 
@@ -88,8 +112,23 @@ django.add_database_administrator(
88
112
  )
89
113
  ```
90
114
 
91
- ## Deployment steps
115
+ ## Changes to your Django project
116
+ 1. Add `pulumi_django_azure` to your `INSTALLED_APPS`
117
+ 2. Add to your settings file:
118
+ ```python
119
+ from pulumi_django_azure.settings import * # noqa: F403
120
+
121
+ # This will provide the management command to purge the CDN
122
+ INSTALLED_APPS += ["pulumi_django_azure"]
123
+
124
+ # This will provide the health check middleware that will also take care of credential rotation.
125
+ MIDDLEWARE += ["pulumi_django_azure.middleware.HealthCheckMiddleware"]
126
+ ```
127
+ This will pre-configure most settings to make your app work on Azure. You can check the source for details,
128
+ and ofcourse override any value after importing them.
129
+
92
130
 
131
+ ## Deployment steps
93
132
  1. Deploy without custom hosts (for CDN and websites)
94
133
  2. Configure the PostgreSQL server (create and grant permissions to role for your websites)
95
134
  3. Retrieve the deployment SSH key and configure your remote GIT repository with it
@@ -172,19 +211,8 @@ The environment variable will be suffixed with `_SECRET_NAME`.
172
211
 
173
212
  Then, in your application, retrieve this data from the vault, e.g.:
174
213
  ```python
175
- from azure.keyvault.secrets import SecretClient
176
- from azure.identity import DefaultAzureCredential
177
-
178
- # Azure credentials
179
- azure_credential = DefaultAzureCredential()
180
-
181
- # Azure Key Vault
182
- AZURE_KEY_VAULT = env("AZURE_KEY_VAULT")
183
- AZURE_KEY_VAULT_URI = f"https://{AZURE_KEY_VAULT}.vault.azure.net"
184
- azure_key_vault_client = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=azure_credential)
185
-
186
214
  # Social Auth settings
187
- oauth_secret = azure_key_vault_client.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
215
+ oauth_secret = AZURE_KEY_VAULT_CLIENT.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
188
216
  oauth_secret = json.loads(oauth_secret.value)
189
217
  SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"]
190
218
  SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"]
@@ -218,81 +246,6 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
218
246
 
219
247
  This would then trigger a redeploy everytime you make a commit to your live branch.
220
248
 
221
- ## CDN Purging
222
- We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
223
- ```python
224
- import os
225
-
226
- from azure.mgmt.cdn import CdnManagementClient
227
- from azure.mgmt.cdn.models import PurgeParameters
228
- from django.core.management.base import BaseCommand
229
-
230
- from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
231
-
232
-
233
- class Command(BaseCommand):
234
- help = "Purges the CDN endpoint"
235
-
236
- def add_arguments(self, parser):
237
- parser.add_argument(
238
- "--wait",
239
- action="store_true",
240
- help="Wait for the purge operation to complete",
241
- )
242
-
243
- def handle(self, *args, **options):
244
- # Read environment variables
245
- resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
246
- profile_name = os.getenv("CDN_PROFILE")
247
- endpoint_name = os.getenv("CDN_ENDPOINT")
248
- content_paths = ["/*"]
249
-
250
- # Ensure all required environment variables are set
251
- if not all([resource_group, profile_name, endpoint_name]):
252
- self.stderr.write(self.style.ERROR("Missing required environment variables."))
253
- return
254
-
255
- # Authenticate with Azure
256
- cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
257
-
258
- try:
259
- # Purge the CDN endpoint
260
- purge_operation = cdn_client.endpoints.begin_purge_content(
261
- resource_group_name=resource_group,
262
- profile_name=profile_name,
263
- endpoint_name=endpoint_name,
264
- content_file_paths=PurgeParameters(content_paths=content_paths),
265
- )
266
-
267
- # Check if the --wait argument was provided
268
- if options["wait"]:
269
- purge_operation.result() # Wait for the operation to complete
270
- self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
271
- else:
272
- self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
273
-
274
- except Exception as e:
275
- self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
276
- ```
277
-
278
- And our azure_helper:
279
- ```python
280
- from azure.identity import DefaultAzureCredential
281
- from azure.mgmt.resource import SubscriptionClient
282
-
283
- # Azure credentials
284
- AZURE_CREDENTIAL = DefaultAzureCredential()
285
-
286
-
287
- def get_db_password() -> str:
288
- return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
289
-
290
-
291
- def get_subscription_id() -> str:
292
- subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
293
- subscriptions = list(subscription_client.subscriptions.list())
294
- return subscriptions[0].subscription_id
295
- ```
296
249
 
297
250
  ## Change requests
298
251
  I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pulumi-django-azure"
7
+ version = "1.0.23"
8
+ description = "Simply deployment of Django on Azure with Pulumi"
9
+ readme = "README.md"
10
+ authors = [{ name = "Maarten Ureel", email = "maarten@youreal.eu" }]
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ keywords = ["django", "pulumi", "azure"]
18
+ dependencies = [
19
+ "azure-identity (>=1.21.0,<2.0.0)",
20
+ "azure-keyvault-secrets (>=4.9.0,<5.0.0)",
21
+ "azure-mgmt-cdn (>=13.1.1,<14.0.0)",
22
+ "azure-mgmt-resource (>=23.3.0,<24.0.0)",
23
+ "django (>=5.1.7,<6.0.0)",
24
+ "django-azure-communication-email (>=1.3.0,<2.0.0)",
25
+ "django-environ (>=0.12.0,<0.13.0)",
26
+ "django-redis (>=5.4.0,<6.0.0)",
27
+ "django-storages[azure] (>=1.14.5,<2.0.0)",
28
+ "pulumi (>=3.156.0)",
29
+ "pulumi-azure-native (>=2.89.1)",
30
+ "pulumi-random (>=4.18.0)",
31
+ "redis[hiredis] (>=5.2.1,<6.0.0)",
32
+ ]
33
+ requires-python = ">=3.11,<3.14"
34
+
35
+ [project.urls]
36
+ Homepage = "https://gitlab.com/MaartenUreel/pulumi-django-azure"
37
+
38
+ [tool.poetry]
39
+ name = "pulumi-django-azure"
40
+ version = "1.0.23"
41
+ description = "Simply deployment of Django on Azure with Pulumi"
42
+ authors = ["Maarten Ureel <maarten@youreal.eu>"]
43
+
44
+ [tool.poetry.dependencies]
45
+ python = "^3.11,<3.14"
46
+ pulumi-azure-native = ">=2.89.1"
47
+ pulumi = ">=3.156.0"
48
+ pulumi-random = ">=4.18.0"
49
+ django-storages = {extras = ["azure"], version = "^1.14.5"}
50
+ azure-identity = "^1.21.0"
51
+ azure-keyvault-secrets = "^4.9.0"
52
+ azure-mgmt-cdn = "^13.1.1"
53
+ azure-mgmt-resource = "^23.3.0"
54
+ django = "^5.1.7"
55
+ django-azure-communication-email = "^1.3.0"
56
+ django-environ = "^0.12.0"
57
+ django-redis = "^5.4.0"
58
+ redis = {extras = ["hiredis"], version = "^5.2.1"}
59
+
60
+ [tool.poetry.group.dev.dependencies]
61
+ twine = "^6.1.0"
62
+ build = "^1.2.2.post1"
63
+ ruff = "^0.11.0"
64
+
65
+ [tool.ruff]
66
+ line-length = 140
67
+
68
+ [tool.ruff.lint]
69
+ select = [
70
+ # pycodestyle
71
+ "E",
72
+ # Pyflakes
73
+ "F",
74
+ # pyupgrade
75
+ "UP",
76
+ # flake8-bugbear
77
+ "B",
78
+ # flake8-simplify
79
+ "SIM",
80
+ # isort
81
+ "I",
82
+ ]
@@ -1 +1,2 @@
1
+ from . import settings # noqa: F401
1
2
  from .django_deployment import DjangoDeployment, HostDefinition # noqa: F401
@@ -0,0 +1,67 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ from dataclasses import dataclass
5
+ from subprocess import check_output
6
+
7
+ from azure.identity import DefaultAzureCredential
8
+ from azure.mgmt.resource import SubscriptionClient
9
+ from azure.mgmt.resource.subscriptions.models import Subscription
10
+
11
+ # Azure credentials
12
+ AZURE_CREDENTIAL = DefaultAzureCredential()
13
+
14
+ # Get the local IP addresses of the machine (only when runnig on Azure)
15
+ if os.environ.get("ORYX_ENV_NAME"):
16
+ LOCAL_IP_ADDRESSES = check_output(["hostname", "--all-ip-addresses"]).decode("utf-8").strip().split(" ")
17
+ else:
18
+ LOCAL_IP_ADDRESSES = []
19
+
20
+
21
+ def get_db_password() -> str:
22
+ """
23
+ Get a valid password for the database.
24
+ """
25
+ return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
26
+
27
+
28
+ @dataclass
29
+ class RedisCredentials:
30
+ username: str
31
+ password: str
32
+
33
+
34
+ def get_redis_credentials() -> RedisCredentials:
35
+ """
36
+ Get valid credentials for the Redis cache.
37
+ """
38
+ token = AZURE_CREDENTIAL.get_token("https://redis.azure.com/.default").token
39
+ return RedisCredentials(_extract_username_from_token(token), token)
40
+
41
+
42
+ def get_subscription() -> Subscription:
43
+ """
44
+ Get the subscription for the current user.
45
+ """
46
+ subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
47
+ subscriptions = list(subscription_client.subscriptions.list())
48
+ return subscriptions[0]
49
+
50
+
51
+ def _extract_username_from_token(token: str) -> str:
52
+ """
53
+ Extract the username from the JSON Web Token (JWT) token.
54
+ """
55
+ parts = token.split(".")
56
+ base64_str = parts[1]
57
+
58
+ if len(base64_str) % 4 == 2:
59
+ base64_str += "=="
60
+ elif len(base64_str) % 4 == 3:
61
+ base64_str += "="
62
+
63
+ json_bytes = base64.b64decode(base64_str)
64
+ json_str = json_bytes.decode("utf-8")
65
+ jwt = json.loads(json_str)
66
+
67
+ return jwt["oid"]
@@ -1,5 +1,4 @@
1
1
  from collections.abc import Sequence
2
- from typing import Optional, Union
3
2
 
4
3
  import pulumi
5
4
  import pulumi_azure_native as azure
@@ -15,7 +14,7 @@ class HostDefinition:
15
14
  :param identifier: An identifier for this host definition (optional).
16
15
  """
17
16
 
18
- def __init__(self, host: str, zone: Optional[azure.network.Zone] = None, identifier: Optional[str] = None):
17
+ def __init__(self, host: str, zone: azure.network.Zone | None = None, identifier: str | None = None):
19
18
  self.host = host
20
19
  self.zone = zone
21
20
  self._identifier = identifier
@@ -68,12 +67,12 @@ class DjangoDeployment(pulumi.ComponentResource):
68
67
  app_service_ip_prefix: str,
69
68
  app_service_sku: azure.web.SkuDescriptionArgs,
70
69
  storage_account_name: str,
71
- storage_allowed_origins: Optional[Sequence[str]] = None,
72
- pgadmin_access_ip: Optional[Sequence[str]] = None,
73
- pgadmin_dns_zone: Optional[azure.network.Zone] = None,
74
- cache_ip_prefix: Optional[str] = None,
75
- cache_sku: Optional[azure.cache.SkuArgs] = None,
76
- cdn_host: Optional[HostDefinition] = None,
70
+ storage_allowed_origins: Sequence[str] | None = None,
71
+ pgadmin_access_ip: Sequence[str] | None = None,
72
+ pgadmin_dns_zone: azure.network.Zone | None = None,
73
+ cache_ip_prefix: str | None = None,
74
+ cache_sku: azure.cache.SkuArgs | None = None,
75
+ cdn_host: HostDefinition | None = None,
77
76
  opts=None,
78
77
  ):
79
78
  """
@@ -134,7 +133,7 @@ class DjangoDeployment(pulumi.ComponentResource):
134
133
  # Create a pgAdmin app
135
134
  self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
136
135
 
137
- def _create_storage(self, account_name: str, allowed_origins: Optional[Sequence[str]] = None):
136
+ def _create_storage(self, account_name: str, allowed_origins: Sequence[str] | None = None):
138
137
  # Create blob storage
139
138
  self._storage_account = azure.storage.StorageAccount(
140
139
  f"sa-{self._name}",
@@ -169,7 +168,7 @@ class DjangoDeployment(pulumi.ComponentResource):
169
168
  ),
170
169
  )
171
170
 
172
- def _create_cdn(self, custom_host: Optional[HostDefinition]) -> pulumi.Output[str]:
171
+ def _create_cdn(self, custom_host: HostDefinition | None) -> pulumi.Output[str]:
173
172
  """
174
173
  Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
175
174
  Otherwise, the default CDN host name will be returned.
@@ -392,7 +391,7 @@ class DjangoDeployment(pulumi.ComponentResource):
392
391
  self,
393
392
  name,
394
393
  prefix,
395
- delegation_service: Optional[str] = None,
394
+ delegation_service: str | None = None,
396
395
  service_endpoints: Sequence[str] = [],
397
396
  ) -> azure.network.Subnet:
398
397
  """
@@ -433,7 +432,7 @@ class DjangoDeployment(pulumi.ComponentResource):
433
432
  sku=sku,
434
433
  )
435
434
 
436
- def _create_pgadmin_app(self, access_ip: Optional[Sequence[str]] = None, dns_zone: Optional[azure.network.Zone] = None):
435
+ def _create_pgadmin_app(self, access_ip: Sequence[str] | None = None, dns_zone: azure.network.Zone | None = None):
437
436
  # Determine the IP restrictions
438
437
  ip_restrictions = []
439
438
  default_restriction = azure.web.DefaultAction.ALLOW
@@ -562,10 +561,10 @@ class DjangoDeployment(pulumi.ComponentResource):
562
561
  def _add_webapp_host(
563
562
  self,
564
563
  app: azure.web.WebApp,
565
- host: Union[str, pulumi.Input[str]],
564
+ host: str | pulumi.Input[str],
566
565
  suffix: str,
567
566
  identifier: str,
568
- depends_on: Optional[Sequence[pulumi.Resource]] = None,
567
+ depends_on: Sequence[pulumi.Resource] | None = None,
569
568
  ):
570
569
  """
571
570
  Because of a circular dependency, we need to create the certificate and the binding in two steps.
@@ -838,13 +837,13 @@ class DjangoDeployment(pulumi.ComponentResource):
838
837
  website_hosts: list[HostDefinition],
839
838
  django_settings_module: str,
840
839
  python_version: str = "3.13",
841
- environment_variables: Optional[dict[str, str]] = None,
842
- secrets: Optional[dict[str, str]] = None,
843
- comms_data_location: Optional[str] = None,
844
- comms_domains: Optional[list[HostDefinition]] = None,
845
- dedicated_app_service_sku: Optional[azure.web.SkuDescriptionArgs] = None,
846
- vault_administrators: Optional[list[str]] = None,
847
- cache_db: Optional[int] = None,
840
+ environment_variables: dict[str, str] | None = None,
841
+ secrets: dict[str, str] | None = None,
842
+ comms_data_location: str | None = None,
843
+ comms_domains: list[HostDefinition] | None = None,
844
+ dedicated_app_service_sku: azure.web.SkuDescriptionArgs | None = None,
845
+ vault_administrators: list[str] | None = None,
846
+ cache_db: int | None = None,
848
847
  ) -> azure.web.WebApp:
849
848
  """
850
849
  Create a Django website with it's own database and storage containers.
@@ -0,0 +1,52 @@
1
+ import os
2
+
3
+ from azure.mgmt.cdn import CdnManagementClient
4
+ from azure.mgmt.cdn.models import PurgeParameters
5
+ from django.conf import settings
6
+ from django.core.management.base import BaseCommand
7
+ from pulumi_django_azure.azure_helper import AZURE_CREDENTIAL
8
+
9
+
10
+ class Command(BaseCommand):
11
+ help = "Purges the CDN endpoint"
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument(
15
+ "--wait",
16
+ action="store_true",
17
+ help="Wait for the purge operation to complete",
18
+ )
19
+
20
+ def handle(self, *args, **options):
21
+ # Read environment variables
22
+ resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
23
+ profile_name = os.getenv("CDN_PROFILE")
24
+ endpoint_name = os.getenv("CDN_ENDPOINT")
25
+ content_paths = ["/*"]
26
+
27
+ # Ensure all required environment variables are set
28
+ if not all([resource_group, profile_name, endpoint_name]):
29
+ self.stderr.write(self.style.ERROR("Missing required environment variables."))
30
+ return
31
+
32
+ # Authenticate with Azure
33
+ cdn_client = CdnManagementClient(AZURE_CREDENTIAL, settings.AZURE_SUBSCRIPTION_ID)
34
+
35
+ try:
36
+ # Purge the CDN endpoint
37
+ purge_operation = cdn_client.endpoints.begin_purge_content(
38
+ resource_group_name=resource_group,
39
+ profile_name=profile_name,
40
+ endpoint_name=endpoint_name,
41
+ content_file_paths=PurgeParameters(content_paths=content_paths),
42
+ )
43
+
44
+ # Check if the --wait argument was provided
45
+ if options["wait"]:
46
+ purge_operation.result() # Wait for the operation to complete
47
+ self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
48
+ else:
49
+ self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
50
+
51
+ except Exception as e:
52
+ self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
@@ -0,0 +1,43 @@
1
+ from django.conf import settings
2
+ from django.core.cache import cache
3
+ from django.db import connection
4
+ from django.db.utils import OperationalError
5
+ from django.http import HttpResponse
6
+ from django_redis import get_redis_connection
7
+
8
+ from .azure_helper import get_db_password, get_redis_credentials
9
+
10
+
11
+ class HealthCheckMiddleware:
12
+ def __init__(self, get_response):
13
+ self.get_response = get_response
14
+
15
+ def __call__(self, request):
16
+ if request.path == settings.HEALTH_CHECK_PATH:
17
+ # Update the database credentials if needed
18
+ if settings.AZURE_DB_PASSWORD:
19
+ settings.DATABASES["default"]["PASSWORD"] = get_db_password()
20
+
21
+ # Update the Redis credentials if needed
22
+ if settings.AZURE_REDIS_CREDENTIALS:
23
+ redis_credentials = get_redis_credentials()
24
+
25
+ # Re-authenticate the Redis connection
26
+ redis_connection = get_redis_connection("default")
27
+ redis_connection.execute_command("AUTH", redis_credentials.username, redis_credentials.password)
28
+
29
+ settings.CACHES["default"]["OPTIONS"]["PASSWORD"] = redis_credentials.password
30
+
31
+ try:
32
+ # Test the database connection
33
+ connection.ensure_connection()
34
+
35
+ # Test the Redis connection
36
+ cache.set("health_check", "test")
37
+
38
+ return HttpResponse("OK")
39
+
40
+ except OperationalError:
41
+ return HttpResponse(status=503)
42
+
43
+ return self.get_response(request)
@@ -0,0 +1,154 @@
1
+ import environ
2
+ from azure.keyvault.secrets import SecretClient
3
+
4
+ from .azure_helper import AZURE_CREDENTIAL, LOCAL_IP_ADDRESSES, get_db_password, get_redis_credentials, get_subscription
5
+
6
+ env = environ.Env()
7
+
8
+ SECRET_KEY = env("DJANGO_SECRET_KEY")
9
+
10
+ IS_AZURE_ENVIRONMENT = env("ORYX_ENV_NAME", default=None) is not None
11
+
12
+ # Some generic stuff we only need if we're running in Azure.
13
+ # Most of the other stuff will check for the explicit variable we need.
14
+ if IS_AZURE_ENVIRONMENT:
15
+ # Detect HTTPS behind AppService
16
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
17
+
18
+ # Azure context
19
+ AZURE_SUBSCRIPTION = get_subscription()
20
+ AZURE_TENANT_ID = AZURE_SUBSCRIPTION.tenant_id
21
+ AZURE_SUBSCRIPTION_ID = AZURE_SUBSCRIPTION.subscription_id
22
+
23
+ # Health check path
24
+ HEALTH_CHECK_PATH = env("HEALTH_CHECK_PATH", default="/health")
25
+
26
+ ALLOWED_HOSTS: list = env.list("DJANGO_ALLOWED_HOSTS", default=[])
27
+
28
+ # WEBSITE_HOSTNAME contains the Azure domain name
29
+ if website_hostname := env("WEBSITE_HOSTNAME", default=None):
30
+ ALLOWED_HOSTS.append(website_hostname)
31
+
32
+ # Add the local IP addresses of the machine for health checks
33
+ ALLOWED_HOSTS.extend(LOCAL_IP_ADDRESSES)
34
+
35
+ # Azure Key Vault
36
+ if azure_key_vault := env("AZURE_KEY_VAULT", default=None):
37
+ AZURE_KEY_VAULT_URI = f"https://{azure_key_vault}.vault.azure.net"
38
+ AZURE_KEY_VAULT_CLIENT = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=AZURE_CREDENTIAL)
39
+
40
+ # Allow CSRF cookies to be sent from our domains
41
+ # CSRF_TRUSTED_ORIGINS = ["https://" + host for host in ALLOWED_HOSTS]
42
+ if azure_storage_account_name := env("AZURE_STORAGE_ACCOUNT_NAME", default=None):
43
+ AZURE_ACCOUNT_NAME = azure_storage_account_name
44
+ AZURE_TOKEN_CREDENTIAL = AZURE_CREDENTIAL
45
+
46
+
47
+ # CDN domain - shared for all storages
48
+ if cdn_host := env("CDN_HOST", default=None):
49
+ AZURE_CUSTOM_DOMAIN = cdn_host
50
+
51
+ STATIC_URL = f"https://{AZURE_CUSTOM_DOMAIN}/static/"
52
+ MEDIA_URL = f"https://{AZURE_CUSTOM_DOMAIN}/media/"
53
+
54
+ # Storage configuration
55
+ STORAGES = {}
56
+ if container_media := env("AZURE_STORAGE_CONTAINER_MEDIA", default=None):
57
+ STORAGES["default"] = {
58
+ "BACKEND": "storages.backends.azure_storage.AzureStorage",
59
+ "OPTIONS": {
60
+ "azure_container": container_media,
61
+ "overwrite_files": False,
62
+ },
63
+ }
64
+
65
+ if container_staticfiles := env("AZURE_STORAGE_CONTAINER_STATICFILES", default=None):
66
+ STORAGES["staticfiles"] = {
67
+ "BACKEND": "storages.backends.azure_storage.AzureStorage",
68
+ "OPTIONS": {
69
+ "azure_container": container_staticfiles,
70
+ },
71
+ }
72
+
73
+ # This setting enables password rotation in the health check middleware
74
+ if IS_AZURE_ENVIRONMENT:
75
+ AZURE_DB_PASSWORD = True
76
+ DATABASES = {
77
+ "default": {
78
+ "ENGINE": "django.db.backends.postgresql",
79
+ "NAME": env("DB_NAME"),
80
+ "USER": env("DB_USER"),
81
+ "HOST": env("DB_HOST"),
82
+ "PASSWORD": get_db_password(),
83
+ "PORT": "5432",
84
+ "OPTIONS": {
85
+ "sslmode": "require",
86
+ },
87
+ # Make connections persistent
88
+ "CONN_MAX_AGE": None,
89
+ # To enable health checks, add the following:
90
+ # "CONN_HEALTH_CHECKS": True,
91
+ }
92
+ }
93
+
94
+ # Email
95
+ EMAIL_BACKEND = "django_azure_communication_email.EmailBackend"
96
+ AZURE_COMMUNICATION_ENDPOINT = env("AZURE_COMMUNICATION_SERVICE_ENDPOINT", default=None)
97
+ DEFAULT_FROM_EMAIL = env("DJANGO_DEFAULT_FROM_EMAIL", default=None)
98
+
99
+
100
+ # Logging
101
+ if IS_AZURE_ENVIRONMENT:
102
+ LOGGING = {
103
+ "version": 1,
104
+ "disable_existing_loggers": False,
105
+ "handlers": {
106
+ "file": {
107
+ "level": "INFO",
108
+ "class": "logging.handlers.TimedRotatingFileHandler",
109
+ "filename": "/home/LogFiles/django.log",
110
+ "when": "h",
111
+ "interval": 1,
112
+ "backupCount": 24,
113
+ },
114
+ },
115
+ "loggers": {
116
+ "django": {
117
+ "handlers": ["file"],
118
+ "level": "INFO",
119
+ "propagate": True,
120
+ },
121
+ },
122
+ }
123
+
124
+ # Redis, if enabled
125
+ redis_cache_host = env("REDIS_CACHE_HOST", default=None)
126
+ redis_cache_port = env("REDIS_CACHE_PORT", default=None)
127
+ redis_cache_db = env("REDIS_CACHE_DB", default=None)
128
+
129
+ if redis_cache_host and redis_cache_port and redis_cache_db:
130
+ # This will enable the health check to update the Redis credentials
131
+ AZURE_REDIS_CREDENTIALS = True
132
+
133
+ # This will prevent the website from failing if Redis is not available
134
+ DJANGO_REDIS_IGNORE_EXCEPTIONS = True
135
+ DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
136
+
137
+ REDIS_CACHE_HOST = redis_cache_host
138
+ REDIS_CACHE_PORT = redis_cache_port
139
+ REDIS_CACHE_DB = redis_cache_db
140
+ redis_credentials = get_redis_credentials()
141
+ REDIS_USERNAME = redis_credentials.username
142
+ REDIS_PASSWORD = redis_credentials.password
143
+
144
+ CACHES = {
145
+ "default": {
146
+ "BACKEND": "django_redis.cache.RedisCache",
147
+ "LOCATION": f"rediss://{REDIS_USERNAME}@{REDIS_CACHE_HOST}:{REDIS_CACHE_PORT}/{REDIS_CACHE_DB}",
148
+ "OPTIONS": {
149
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
150
+ "PARSER_CLASS": "redis.connection._HiredisParser",
151
+ "PASSWORD": REDIS_PASSWORD,
152
+ },
153
+ },
154
+ }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.21
3
+ Version: 1.0.23
4
4
  Summary: Simply deployment of Django on Azure with Pulumi
5
5
  Author-email: Maarten Ureel <maarten@youreal.eu>
6
6
  License: MIT License
@@ -30,12 +30,23 @@ Keywords: django,pulumi,azure
30
30
  Classifier: License :: OSI Approved :: MIT License
31
31
  Classifier: Programming Language :: Python
32
32
  Classifier: Programming Language :: Python :: 3
33
- Requires-Python: >=3.9
33
+ Requires-Python: <3.14,>=3.11
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: pulumi>=3.146.0
37
- Requires-Dist: pulumi-azure-native>=2.82.0
38
- Requires-Dist: pulumi-random>=4.17.0
36
+ Requires-Dist: azure-identity<2.0.0,>=1.21.0
37
+ Requires-Dist: azure-keyvault-secrets<5.0.0,>=4.9.0
38
+ Requires-Dist: azure-mgmt-cdn<14.0.0,>=13.1.1
39
+ Requires-Dist: azure-mgmt-resource<24.0.0,>=23.3.0
40
+ Requires-Dist: django<6.0.0,>=5.1.7
41
+ Requires-Dist: django-azure-communication-email<2.0.0,>=1.3.0
42
+ Requires-Dist: django-environ<0.13.0,>=0.12.0
43
+ Requires-Dist: django-redis<6.0.0,>=5.4.0
44
+ Requires-Dist: django-storages[azure]<2.0.0,>=1.14.5
45
+ Requires-Dist: pulumi>=3.156.0
46
+ Requires-Dist: pulumi-azure-native>=2.89.1
47
+ Requires-Dist: pulumi-random>=4.18.0
48
+ Requires-Dist: redis[hiredis]<6.0.0,>=5.2.1
49
+ Dynamic: license-file
39
50
 
40
51
  # Pulumi Django Deployment
41
52
 
@@ -57,12 +68,36 @@ Your Django project should contain a folder `cicd` with these files:
57
68
  Note that this runs in the identity of the build container, so you should not run database or storage manipulations here.
58
69
  * startup.sh: commands to run the actual application. I recommend to put at least:
59
70
  ```bash
71
+ # Compile translations
72
+ apt-get -y install gettext
73
+ python manage.py compilemessages
74
+
75
+ # Run collectstatic in the background
76
+ sh cicd/collectstatic.sh &
77
+
60
78
  python manage.py migrate
61
- python manage.py collectstatic --noinput
62
- python manage.py purge_cdn
63
79
  gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
64
80
  ```
65
- Be sure to change `yourapplication` in the above. To see the `purge_cdn` management command we use here, see below.
81
+
82
+ Be sure to change `yourapplication` in the above.
83
+
84
+ * collecstatic.sh
85
+ ```bash
86
+ #!/bin/bash
87
+
88
+ LOGFILE="/var/log/django_collectstatic.log"
89
+
90
+ # Collectstatic
91
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting collectstatic" >> $LOGFILE
92
+ python manage.py collectstatic --noinput -v 2 >> $LOGFILE
93
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished collectstatic" >> $LOGFILE
94
+
95
+ # Purge the CDN
96
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Starting purge_cdn" >> $LOGFILE
97
+ python manage.py purge_cdn >> $LOGFILE
98
+ echo "$(date '+%Y-%m-%d %H:%M:%S') - Finished purge_cdn" >> $LOGFILE
99
+ ```
100
+
66
101
  ## Installation
67
102
  This package is published on PyPi, so you can just add pulumi-django-azure to your requirements file.
68
103
 
@@ -127,8 +162,23 @@ django.add_database_administrator(
127
162
  )
128
163
  ```
129
164
 
130
- ## Deployment steps
165
+ ## Changes to your Django project
166
+ 1. Add `pulumi_django_azure` to your `INSTALLED_APPS`
167
+ 2. Add to your settings file:
168
+ ```python
169
+ from pulumi_django_azure.settings import * # noqa: F403
170
+
171
+ # This will provide the management command to purge the CDN
172
+ INSTALLED_APPS += ["pulumi_django_azure"]
173
+
174
+ # This will provide the health check middleware that will also take care of credential rotation.
175
+ MIDDLEWARE += ["pulumi_django_azure.middleware.HealthCheckMiddleware"]
176
+ ```
177
+ This will pre-configure most settings to make your app work on Azure. You can check the source for details,
178
+ and ofcourse override any value after importing them.
179
+
131
180
 
181
+ ## Deployment steps
132
182
  1. Deploy without custom hosts (for CDN and websites)
133
183
  2. Configure the PostgreSQL server (create and grant permissions to role for your websites)
134
184
  3. Retrieve the deployment SSH key and configure your remote GIT repository with it
@@ -211,19 +261,8 @@ The environment variable will be suffixed with `_SECRET_NAME`.
211
261
 
212
262
  Then, in your application, retrieve this data from the vault, e.g.:
213
263
  ```python
214
- from azure.keyvault.secrets import SecretClient
215
- from azure.identity import DefaultAzureCredential
216
-
217
- # Azure credentials
218
- azure_credential = DefaultAzureCredential()
219
-
220
- # Azure Key Vault
221
- AZURE_KEY_VAULT = env("AZURE_KEY_VAULT")
222
- AZURE_KEY_VAULT_URI = f"https://{AZURE_KEY_VAULT}.vault.azure.net"
223
- azure_key_vault_client = SecretClient(vault_url=AZURE_KEY_VAULT_URI, credential=azure_credential)
224
-
225
264
  # Social Auth settings
226
- oauth_secret = azure_key_vault_client.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
265
+ oauth_secret = AZURE_KEY_VAULT_CLIENT.get_secret(env("AZURE_OAUTH_SECRET_NAME"))
227
266
  oauth_secret = json.loads(oauth_secret.value)
228
267
  SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"]
229
268
  SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"]
@@ -257,81 +296,6 @@ Be sure to configure the SSH key that Azure will use on GitLab side. You can obt
257
296
 
258
297
  This would then trigger a redeploy everytime you make a commit to your live branch.
259
298
 
260
- ## CDN Purging
261
- We added a management command to Django to purge the CDN cache, and added that to the startup script. Our version is here:
262
- ```python
263
- import os
264
-
265
- from azure.mgmt.cdn import CdnManagementClient
266
- from azure.mgmt.cdn.models import PurgeParameters
267
- from django.core.management.base import BaseCommand
268
-
269
- from core.azure_helper import AZURE_CREDENTIAL, get_subscription_id
270
-
271
-
272
- class Command(BaseCommand):
273
- help = "Purges the CDN endpoint"
274
-
275
- def add_arguments(self, parser):
276
- parser.add_argument(
277
- "--wait",
278
- action="store_true",
279
- help="Wait for the purge operation to complete",
280
- )
281
-
282
- def handle(self, *args, **options):
283
- # Read environment variables
284
- resource_group = os.getenv("WEBSITE_RESOURCE_GROUP")
285
- profile_name = os.getenv("CDN_PROFILE")
286
- endpoint_name = os.getenv("CDN_ENDPOINT")
287
- content_paths = ["/*"]
288
-
289
- # Ensure all required environment variables are set
290
- if not all([resource_group, profile_name, endpoint_name]):
291
- self.stderr.write(self.style.ERROR("Missing required environment variables."))
292
- return
293
-
294
- # Authenticate with Azure
295
- cdn_client = CdnManagementClient(AZURE_CREDENTIAL, get_subscription_id())
296
-
297
- try:
298
- # Purge the CDN endpoint
299
- purge_operation = cdn_client.endpoints.begin_purge_content(
300
- resource_group_name=resource_group,
301
- profile_name=profile_name,
302
- endpoint_name=endpoint_name,
303
- content_file_paths=PurgeParameters(content_paths=content_paths),
304
- )
305
-
306
- # Check if the --wait argument was provided
307
- if options["wait"]:
308
- purge_operation.result() # Wait for the operation to complete
309
- self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation completed successfully."))
310
- else:
311
- self.stdout.write(self.style.SUCCESS("CDN endpoint purge operation started successfully."))
312
-
313
- except Exception as e:
314
- self.stderr.write(self.style.ERROR(f"Error executing CDN endpoint purge command: {e}"))
315
- ```
316
-
317
- And our azure_helper:
318
- ```python
319
- from azure.identity import DefaultAzureCredential
320
- from azure.mgmt.resource import SubscriptionClient
321
-
322
- # Azure credentials
323
- AZURE_CREDENTIAL = DefaultAzureCredential()
324
-
325
-
326
- def get_db_password() -> str:
327
- return AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default").token
328
-
329
-
330
- def get_subscription_id() -> str:
331
- subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
332
- subscriptions = list(subscription_client.subscriptions.list())
333
- return subscriptions[0].subscription_id
334
- ```
335
299
 
336
300
  ## Change requests
337
301
  I created this for internal use but since it took me a while to puzzle all the things together I decided to share it.
@@ -3,9 +3,14 @@ README.md
3
3
  pyproject.toml
4
4
  setup.cfg
5
5
  src/pulumi_django_azure/__init__.py
6
+ src/pulumi_django_azure/azure_helper.py
6
7
  src/pulumi_django_azure/django_deployment.py
8
+ src/pulumi_django_azure/middleware.py
9
+ src/pulumi_django_azure/settings.py
7
10
  src/pulumi_django_azure.egg-info/PKG-INFO
8
11
  src/pulumi_django_azure.egg-info/SOURCES.txt
9
12
  src/pulumi_django_azure.egg-info/dependency_links.txt
10
13
  src/pulumi_django_azure.egg-info/requires.txt
11
- src/pulumi_django_azure.egg-info/top_level.txt
14
+ src/pulumi_django_azure.egg-info/top_level.txt
15
+ src/pulumi_django_azure/management/commands/__init__.py
16
+ src/pulumi_django_azure/management/commands/purge_cdn.py
@@ -0,0 +1,13 @@
1
+ azure-identity<2.0.0,>=1.21.0
2
+ azure-keyvault-secrets<5.0.0,>=4.9.0
3
+ azure-mgmt-cdn<14.0.0,>=13.1.1
4
+ azure-mgmt-resource<24.0.0,>=23.3.0
5
+ django<6.0.0,>=5.1.7
6
+ django-azure-communication-email<2.0.0,>=1.3.0
7
+ django-environ<0.13.0,>=0.12.0
8
+ django-redis<6.0.0,>=5.4.0
9
+ django-storages[azure]<2.0.0,>=1.14.5
10
+ pulumi>=3.156.0
11
+ pulumi-azure-native>=2.89.1
12
+ pulumi-random>=4.18.0
13
+ redis[hiredis]<6.0.0,>=5.2.1
@@ -1,62 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "pulumi-django-azure"
7
- version = "1.0.21"
8
- description = "Simply deployment of Django on Azure with Pulumi"
9
- readme = "README.md"
10
- authors = [{ name = "Maarten Ureel", email = "maarten@youreal.eu" }]
11
- license = { file = "LICENSE" }
12
- classifiers = [
13
- "License :: OSI Approved :: MIT License",
14
- "Programming Language :: Python",
15
- "Programming Language :: Python :: 3",
16
- ]
17
- keywords = ["django", "pulumi", "azure"]
18
- dependencies = [
19
- "pulumi (>=3.146.0)",
20
- "pulumi-azure-native (>=2.82.0)",
21
- "pulumi-random (>=4.17.0)",
22
- ]
23
- requires-python = ">=3.9"
24
-
25
- [project.urls]
26
- Homepage = "https://gitlab.com/MaartenUreel/pulumi-django-azure"
27
-
28
- [tool.poetry]
29
- name = "pulumi-django-azure"
30
- version = "1.0.21"
31
- description = "Simply deployment of Django on Azure with Pulumi"
32
- authors = ["Maarten Ureel <maarten@youreal.eu>"]
33
-
34
- [tool.poetry.dependencies]
35
- python = "^3.11"
36
- pulumi-azure-native = ">=2.82.0"
37
- pulumi = ">=3.146.0"
38
- pulumi-random = ">=4.17.0"
39
-
40
- [tool.poetry.group.dev.dependencies]
41
- twine = "^6.0.1"
42
- build = "^1.2.2.post1"
43
- ruff = "^0.9.2"
44
-
45
- [tool.ruff]
46
- line-length = 140
47
-
48
- [tool.ruff.lint]
49
- select = [
50
- # pycodestyle
51
- "E",
52
- # Pyflakes
53
- "F",
54
- # pyupgrade
55
- "UP",
56
- # flake8-bugbear
57
- "B",
58
- # flake8-simplify
59
- "SIM",
60
- # isort
61
- "I",
62
- ]
@@ -1,3 +0,0 @@
1
- pulumi>=3.146.0
2
- pulumi-azure-native>=2.82.0
3
- pulumi-random>=4.17.0