pulumi-django-azure 1.0.26__tar.gz → 1.0.28__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.26/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.28}/PKG-INFO +10 -34
  2. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/README.md +2 -2
  3. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/pyproject.toml +20 -20
  4. pulumi_django_azure-1.0.28/src/pulumi_django_azure/azure_helper.py +114 -0
  5. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure/django_deployment.py +50 -36
  6. pulumi_django_azure-1.0.28/src/pulumi_django_azure/management/commands/purge_cache.py +15 -0
  7. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure/management/commands/purge_cdn.py +1 -0
  8. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure/middleware.py +31 -8
  9. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure/settings.py +16 -4
  10. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28/src/pulumi_django_azure.egg-info}/PKG-INFO +10 -34
  11. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure.egg-info/SOURCES.txt +1 -1
  12. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure.egg-info/requires.txt +6 -6
  13. pulumi_django_azure-1.0.26/LICENSE +0 -21
  14. pulumi_django_azure-1.0.26/src/pulumi_django_azure/azure_helper.py +0 -67
  15. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/setup.cfg +0 -0
  16. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure/__init__.py +0 -0
  17. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure/management/commands/__init__.py +0 -0
  18. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure.egg-info/dependency_links.txt +0 -0
  19. {pulumi_django_azure-1.0.26 → pulumi_django_azure-1.0.28}/src/pulumi_django_azure.egg-info/top_level.txt +0 -0
@@ -1,52 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.26
3
+ Version: 1.0.28
4
4
  Summary: Simply deployment of Django on Azure with Pulumi
5
5
  Author-email: Maarten Ureel <maarten@youreal.eu>
6
- License: MIT License
7
-
8
- Copyright (c) 2023 YouReal BV
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Homepage, https://gitlab.com/MaartenUreel/pulumi-django-azure
29
8
  Keywords: django,pulumi,azure
30
- Classifier: License :: OSI Approved :: MIT License
31
9
  Classifier: Programming Language :: Python
32
10
  Classifier: Programming Language :: Python :: 3
33
11
  Requires-Python: <3.14,>=3.11
34
12
  Description-Content-Type: text/markdown
35
- License-File: LICENSE
36
13
  Requires-Dist: azure-identity<2.0.0,>=1.21.0
37
14
  Requires-Dist: azure-keyvault-secrets<5.0.0,>=4.9.0
38
15
  Requires-Dist: azure-mgmt-cdn<14.0.0,>=13.1.1
39
16
  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
17
+ Requires-Dist: django<6.0,>=5.2
18
+ Requires-Dist: django-azure-communication-email<2.0.0,>=1.3.2
42
19
  Requires-Dist: django-environ<0.13.0,>=0.12.0
43
20
  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
21
+ Requires-Dist: django-storages[azure]<2.0.0,>=1.14.6
22
+ Requires-Dist: pulumi>=3.165.0
23
+ Requires-Dist: pulumi-azure-native>=3.2.0
24
+ Requires-Dist: pulumi-random>=4.18.1
48
25
  Requires-Dist: redis[hiredis]<6.0.0,>=5.2.1
49
- Dynamic: license-file
50
26
 
51
27
  # Pulumi Django Deployment
52
28
 
@@ -76,7 +52,7 @@ Your Django project should contain a folder `cicd` with these files:
76
52
  sh cicd/collectstatic.sh &
77
53
 
78
54
  python manage.py migrate
79
- gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
55
+ gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --preload --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
80
56
  ```
81
57
 
82
58
  Be sure to change `yourapplication` in the above.
@@ -168,7 +144,7 @@ django.add_database_administrator(
168
144
  ```python
169
145
  from pulumi_django_azure.settings import * # noqa: F403
170
146
 
171
- # This will provide the management command to purge the CDN
147
+ # This will provide the management command to purge the CDN and cache
172
148
  INSTALLED_APPS += ["pulumi_django_azure"]
173
149
 
174
150
  # This will provide the health check middleware that will also take care of credential rotation.
@@ -26,7 +26,7 @@ Your Django project should contain a folder `cicd` with these files:
26
26
  sh cicd/collectstatic.sh &
27
27
 
28
28
  python manage.py migrate
29
- gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
29
+ gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --preload --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
30
30
  ```
31
31
 
32
32
  Be sure to change `yourapplication` in the above.
@@ -118,7 +118,7 @@ django.add_database_administrator(
118
118
  ```python
119
119
  from pulumi_django_azure.settings import * # noqa: F403
120
120
 
121
- # This will provide the management command to purge the CDN
121
+ # This will provide the management command to purge the CDN and cache
122
122
  INSTALLED_APPS += ["pulumi_django_azure"]
123
123
 
124
124
  # This will provide the health check middleware that will also take care of credential rotation.
@@ -4,13 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pulumi-django-azure"
7
- version = "1.0.26"
7
+ version = "1.0.28"
8
8
  description = "Simply deployment of Django on Azure with Pulumi"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Maarten Ureel", email = "maarten@youreal.eu" }]
11
- license = { file = "LICENSE" }
11
+ license = "MIT"
12
12
  classifiers = [
13
- "License :: OSI Approved :: MIT License",
14
13
  "Programming Language :: Python",
15
14
  "Programming Language :: Python :: 3",
16
15
  ]
@@ -20,15 +19,15 @@ dependencies = [
20
19
  "azure-keyvault-secrets (>=4.9.0,<5.0.0)",
21
20
  "azure-mgmt-cdn (>=13.1.1,<14.0.0)",
22
21
  "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)",
22
+ "django (>=5.2,<6.0)",
23
+ "django-azure-communication-email (>=1.3.2,<2.0.0)",
25
24
  "django-environ (>=0.12.0,<0.13.0)",
26
25
  "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)",
26
+ "django-storages[azure] (>=1.14.6,<2.0.0)",
27
+ "pulumi (>=3.165.0)",
28
+ "pulumi-azure-native (>=3.2.0)",
29
+ "pulumi-random (>=4.18.1)",
30
+ "redis[hiredis] (>=5.2.1,<6.0.0)"
32
31
  ]
33
32
  requires-python = ">=3.11,<3.14"
34
33
 
@@ -37,30 +36,31 @@ Homepage = "https://gitlab.com/MaartenUreel/pulumi-django-azure"
37
36
 
38
37
  [tool.poetry]
39
38
  name = "pulumi-django-azure"
40
- version = "1.0.26"
41
- description = "Simply deployment of Django on Azure with Pulumi"
39
+ version = "1.0.28"
40
+ description = "Simplify deployment of Django websites on Azure with Pulumi"
42
41
  authors = ["Maarten Ureel <maarten@youreal.eu>"]
43
42
 
44
43
  [tool.poetry.dependencies]
45
44
  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
45
  azure-identity = "^1.21.0"
51
46
  azure-keyvault-secrets = "^4.9.0"
52
47
  azure-mgmt-cdn = "^13.1.1"
53
48
  azure-mgmt-resource = "^23.3.0"
54
- django = "^5.1.7"
55
- django-azure-communication-email = "^1.3.0"
49
+ django = "^5.2"
50
+ django-azure-communication-email = "^1.3.2"
56
51
  django-environ = "^0.12.0"
57
52
  django-redis = "^5.4.0"
53
+ django-storages = {extras = ["azure"], version = "^1.14.6"}
54
+ pulumi = ">=3.165.0"
55
+ pulumi-azure-native = ">=3.2.0"
56
+ pulumi-random = ">=4.18.1"
58
57
  redis = {extras = ["hiredis"], version = "^5.2.1"}
59
58
 
60
59
  [tool.poetry.group.dev.dependencies]
61
- twine = "^6.1.0"
62
60
  build = "^1.2.2.post1"
63
- ruff = "^0.11.0"
61
+ pre-commit = "^4.2.0"
62
+ ruff = "^0.11.7"
63
+ twine = "^6.1.0"
64
64
 
65
65
  [tool.ruff]
66
66
  line-length = 140
@@ -0,0 +1,114 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+ import os
5
+ import time
6
+ from dataclasses import dataclass
7
+ from subprocess import check_output
8
+
9
+ from azure.identity import DefaultAzureCredential
10
+ from azure.mgmt.resource import SubscriptionClient
11
+ from azure.mgmt.resource.subscriptions.models import Subscription
12
+
13
+ _redis_token_cache = None
14
+ _database_token_cache = None
15
+
16
+ logger = logging.getLogger("pulumi_django_azure.azure_helper")
17
+
18
+
19
+ # Azure credentials
20
+ AZURE_CREDENTIAL = DefaultAzureCredential()
21
+
22
+ # Get the local IP addresses of the machine (only when runnig on Azure)
23
+ if os.environ.get("IS_AZURE_ENVIRONMENT"):
24
+ LOCAL_IP_ADDRESSES = check_output(["hostname", "--all-ip-addresses"]).decode("utf-8").strip().split(" ")
25
+ else:
26
+ LOCAL_IP_ADDRESSES = []
27
+
28
+
29
+ def get_db_password() -> str:
30
+ """
31
+ Get a valid password for the database.
32
+ """
33
+ global _database_token_cache
34
+ _database_token_cache = AZURE_CREDENTIAL.get_token("https://ossrdbms-aad.database.windows.net/.default")
35
+
36
+ logger.debug("New database token: %s", _database_token_cache)
37
+
38
+ return _database_token_cache.token
39
+
40
+
41
+ def db_token_will_expire(treshold=300) -> bool:
42
+ """
43
+ Check if the database token will expire in the next treshold seconds.
44
+ """
45
+ # If the token is not cached, we consider it expired (so a new one will be fetched)
46
+ if _database_token_cache is None:
47
+ return True
48
+
49
+ logger.debug("Database token expires on: %s", _database_token_cache.expires_on)
50
+
51
+ # If the token is cached, check if it will expire in the next treshold seconds
52
+ return _database_token_cache.expires_on - time.time() < treshold
53
+
54
+
55
+ @dataclass
56
+ class RedisCredentials:
57
+ username: str
58
+ password: str
59
+
60
+
61
+ def get_redis_credentials() -> RedisCredentials:
62
+ """
63
+ Get valid credentials for the Redis cache.
64
+ """
65
+ global _redis_token_cache
66
+ _redis_token_cache = AZURE_CREDENTIAL.get_token("https://redis.azure.com/.default")
67
+
68
+ t = _redis_token_cache.token
69
+
70
+ logger.debug("New Redis token: %s", _redis_token_cache)
71
+
72
+ return RedisCredentials(_extract_username_from_token(t), t)
73
+
74
+
75
+ def redis_token_will_expire(treshold=300) -> bool:
76
+ """
77
+ Check if the Redis token will expire in the next treshold seconds.
78
+ """
79
+ # If the token is not cached, we consider it expired (so a new one will be fetched)
80
+ if _redis_token_cache is None:
81
+ return True
82
+
83
+ logger.debug("Redis token expires on: %s", _redis_token_cache.expires_on)
84
+
85
+ # If the token is cached, check if it will expire in the next treshold seconds
86
+ return _redis_token_cache.expires_on - time.time() < treshold
87
+
88
+
89
+ def get_subscription() -> Subscription:
90
+ """
91
+ Get the subscription for the current user.
92
+ """
93
+ subscription_client = SubscriptionClient(AZURE_CREDENTIAL)
94
+ subscriptions = list(subscription_client.subscriptions.list())
95
+ return subscriptions[0]
96
+
97
+
98
+ def _extract_username_from_token(token: str) -> str:
99
+ """
100
+ Extract the username from the JSON Web Token (JWT) token.
101
+ """
102
+ parts = token.split(".")
103
+ base64_str = parts[1]
104
+
105
+ if len(base64_str) % 4 == 2:
106
+ base64_str += "=="
107
+ elif len(base64_str) % 4 == 3:
108
+ base64_str += "="
109
+
110
+ json_bytes = base64.b64decode(base64_str)
111
+ json_str = json_bytes.decode("utf-8")
112
+ jwt = json.loads(json_str)
113
+
114
+ return jwt["oid"]
@@ -14,7 +14,7 @@ class HostDefinition:
14
14
  :param identifier: An identifier for this host definition (optional).
15
15
  """
16
16
 
17
- def __init__(self, host: str, zone: azure.network.Zone | None = None, identifier: str | None = None):
17
+ def __init__(self, host: str, zone: azure.dns.Zone | None = None, identifier: str | None = None):
18
18
  self.host = host
19
19
  self.zone = zone
20
20
  self._identifier = identifier
@@ -68,10 +68,11 @@ class DjangoDeployment(pulumi.ComponentResource):
68
68
  app_service_sku: azure.web.SkuDescriptionArgs,
69
69
  storage_account_name: str,
70
70
  storage_allowed_origins: Sequence[str] | None = None,
71
+ pgsql_parameters: dict[str, str] | None = None,
71
72
  pgadmin_access_ip: Sequence[str] | None = None,
72
- pgadmin_dns_zone: azure.network.Zone | None = None,
73
+ pgadmin_dns_zone: azure.dns.Zone | None = None,
73
74
  cache_ip_prefix: str | None = None,
74
- cache_sku: azure.cache.SkuArgs | None = None,
75
+ cache_sku: azure.redis.SkuArgs | None = None,
75
76
  cdn_host: HostDefinition | None = None,
76
77
  opts=None,
77
78
  ):
@@ -88,6 +89,7 @@ class DjangoDeployment(pulumi.ComponentResource):
88
89
  :param app_service_sku: The SKU for the app service plan.
89
90
  :param storage_account_name: The name of the storage account. Should be unique across Azure.
90
91
  :param storage_allowed_origins: The origins (hosts) to allow access through CORS policy. You can specify '*' to allow all.
92
+ :param pgsql_parameters: The parameters to set on the PostgreSQL server. (optional)
91
93
  :param pgadmin_access_ip: The IP addresses to allow access to pgAdmin. If empty, all IP addresses are allowed.
92
94
  :param pgadmin_dns_zone: The Azure DNS zone to a pgadmin DNS record in. (optional)
93
95
  :param cache_ip_prefix: The IP prefix for the cache subnet. (optional)
@@ -111,7 +113,7 @@ class DjangoDeployment(pulumi.ComponentResource):
111
113
  self._cdn_host = self._create_cdn(custom_host=cdn_host)
112
114
 
113
115
  # PostgreSQL resources
114
- self._create_database(sku=pgsql_sku, ip_prefix=pgsql_ip_prefix)
116
+ self._create_database(sku=pgsql_sku, ip_prefix=pgsql_ip_prefix, parameters=pgsql_parameters)
115
117
 
116
118
  # Cache resources
117
119
  if cache_ip_prefix and cache_sku:
@@ -220,14 +222,14 @@ class DjangoDeployment(pulumi.ComponentResource):
220
222
  if custom_host:
221
223
  if custom_host.zone:
222
224
  # Create a DNS record for the custom host in the given zone
223
- rs = azure.network.RecordSet(
225
+ rs = azure.dns.RecordSet(
224
226
  f"cdn-cname-{self._name}",
225
227
  resource_group_name=self._rg,
226
228
  zone_name=custom_host.zone.name,
227
229
  relative_record_set_name=custom_host.host,
228
230
  record_type="CNAME",
229
231
  ttl=3600,
230
- target_resource=azure.network.SubResourceArgs(
232
+ target_resource=azure.dns.SubResourceArgs(
231
233
  id=self._cdn_endpoint.id,
232
234
  ),
233
235
  )
@@ -257,7 +259,7 @@ class DjangoDeployment(pulumi.ComponentResource):
257
259
  # Return the default CDN host name
258
260
  return self._cdn_endpoint.host_name
259
261
 
260
- def _create_database(self, sku: azure.dbforpostgresql.SkuArgs, ip_prefix: str):
262
+ def _create_database(self, sku: azure.dbforpostgresql.SkuArgs, ip_prefix: str, parameters: dict[str, str]):
261
263
  # Create subnet for PostgreSQL
262
264
  subnet = self._create_subnet(
263
265
  name="pgsql",
@@ -266,7 +268,7 @@ class DjangoDeployment(pulumi.ComponentResource):
266
268
  )
267
269
 
268
270
  # Create private DNS zone
269
- dns = azure.network.PrivateZone(
271
+ dns = azure.privatedns.PrivateZone(
270
272
  f"dns-pgsql-{self._name}",
271
273
  resource_group_name=self._rg,
272
274
  location="global",
@@ -275,7 +277,7 @@ class DjangoDeployment(pulumi.ComponentResource):
275
277
  )
276
278
 
277
279
  # Link the private DNS zone to the VNet in order to make resolving work
278
- azure.network.VirtualNetworkLink(
280
+ azure.privatedns.VirtualNetworkLink(
279
281
  f"vnet-link-pgsql-{self._name}",
280
282
  resource_group_name=self._rg,
281
283
  location="global",
@@ -306,25 +308,37 @@ class DjangoDeployment(pulumi.ComponentResource):
306
308
  ),
307
309
  )
308
310
 
311
+ # Add parameters
312
+ if parameters:
313
+ for name, value in parameters.items():
314
+ azure.dbforpostgresql.Configuration(
315
+ f"pgsql-config-{self._name}-{name}",
316
+ resource_group_name=self._rg,
317
+ server_name=self._pgsql.name,
318
+ source="user-override",
319
+ configuration_name=name,
320
+ value=value,
321
+ )
322
+
309
323
  pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
310
324
 
311
- def _create_cache(self, sku: azure.cache.SkuArgs, ip_prefix: str):
325
+ def _create_cache(self, sku: azure.redis.SkuArgs, ip_prefix: str):
312
326
  # Create a Redis cache
313
- self._cache = azure.cache.Redis(
327
+ self._cache = azure.redis.Redis(
314
328
  f"cache-{self._name}",
315
329
  resource_group_name=self._rg,
316
330
  sku=sku,
317
331
  enable_non_ssl_port=False,
318
- public_network_access=azure.cache.PublicNetworkAccess.DISABLED,
332
+ public_network_access=azure.redis.PublicNetworkAccess.DISABLED,
319
333
  )
320
334
 
321
335
  # Create an access policy that gives us access to the cache
322
- self._cache_access_policy = azure.cache.AccessPolicy(
336
+ self._cache_access_policy = azure.redis.AccessPolicy(
323
337
  f"cache-access-policy-{self._name}",
324
338
  resource_group_name=self._rg,
325
339
  cache_name=self._cache.name,
326
- # Same as the built in Data Contributor policy
327
- permissions="+@all -@dangerous +cluster|info +cluster|nodes +cluster|slots allkeys",
340
+ # Same as the built in Data Contributor policy + flushdb permissions
341
+ permissions="+@all -@dangerous +flushdb +cluster|info +cluster|nodes +cluster|slots allkeys",
328
342
  )
329
343
 
330
344
  # Allocate a subnet for the cache
@@ -334,7 +348,7 @@ class DjangoDeployment(pulumi.ComponentResource):
334
348
  )
335
349
 
336
350
  # Create a private DNS zone for the cache
337
- dns = azure.network.PrivateZone(
351
+ dns = azure.privatedns.PrivateZone(
338
352
  f"dns-cache-{self._name}",
339
353
  resource_group_name=self._rg,
340
354
  location="global",
@@ -342,7 +356,7 @@ class DjangoDeployment(pulumi.ComponentResource):
342
356
  )
343
357
 
344
358
  # Link the private DNS zone to the VNet in order to make resolving work
345
- azure.network.VirtualNetworkLink(
359
+ azure.privatedns.VirtualNetworkLink(
346
360
  f"vnet-link-cache-{self._name}",
347
361
  resource_group_name=self._rg,
348
362
  location="global",
@@ -377,14 +391,14 @@ class DjangoDeployment(pulumi.ComponentResource):
377
391
  )
378
392
 
379
393
  # Create a DNS record for the cache
380
- azure.network.PrivateRecordSet(
394
+ azure.privatedns.PrivateRecordSet(
381
395
  f"dns-a-cache-{self._name}",
382
396
  resource_group_name=self._rg,
383
397
  private_zone_name=dns.name,
384
398
  relative_record_set_name=self._cache.name,
385
399
  record_type="A",
386
400
  ttl=300,
387
- a_records=[azure.network.ARecordArgs(ipv4_address=ip)],
401
+ a_records=[azure.privatedns.ARecordArgs(ipv4_address=ip)],
388
402
  )
389
403
 
390
404
  def _create_subnet(
@@ -432,7 +446,7 @@ class DjangoDeployment(pulumi.ComponentResource):
432
446
  sku=sku,
433
447
  )
434
448
 
435
- def _create_pgadmin_app(self, access_ip: Sequence[str] | None = None, dns_zone: azure.network.Zone | None = None):
449
+ def _create_pgadmin_app(self, access_ip: Sequence[str] | None = None, dns_zone: azure.dns.Zone | None = None):
436
450
  # Determine the IP restrictions
437
451
  ip_restrictions = []
438
452
  default_restriction = azure.web.DefaultAction.ALLOW
@@ -490,20 +504,20 @@ class DjangoDeployment(pulumi.ComponentResource):
490
504
 
491
505
  if dns_zone:
492
506
  # Create a DNS record for the pgAdmin app
493
- cname = azure.network.RecordSet(
507
+ cname = azure.dns.RecordSet(
494
508
  f"dns-cname-pgadmin-{self._name}",
495
509
  resource_group_name=self._rg,
496
510
  zone_name=dns_zone.name,
497
511
  relative_record_set_name="pgadmin",
498
512
  record_type="CNAME",
499
513
  ttl=3600,
500
- cname_record=azure.network.CnameRecordArgs(
514
+ cname_record=azure.dns.CnameRecordArgs(
501
515
  cname=app.default_host_name,
502
516
  ),
503
517
  )
504
518
 
505
519
  # For the certificate validation to work
506
- txt_validation = azure.network.RecordSet(
520
+ txt_validation = azure.dns.RecordSet(
507
521
  f"dns-txt-pgadmin-{self._name}",
508
522
  resource_group_name=self._rg,
509
523
  zone_name=dns_zone.name,
@@ -511,7 +525,7 @@ class DjangoDeployment(pulumi.ComponentResource):
511
525
  record_type="TXT",
512
526
  ttl=3600,
513
527
  txt_records=[
514
- azure.network.TxtRecordArgs(
528
+ azure.dns.TxtRecordArgs(
515
529
  value=[app.custom_domain_verification_id],
516
530
  )
517
531
  ],
@@ -631,11 +645,11 @@ class DjangoDeployment(pulumi.ComponentResource):
631
645
 
632
646
  existing_binding.apply(_create_binding_with_cert)
633
647
 
634
- def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
648
+ def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.dns.RecordSet]:
635
649
  created_records = []
636
650
 
637
651
  # Domain validation and SPF record (one TXT record with multiple values)
638
- r = azure.network.RecordSet(
652
+ r = azure.dns.RecordSet(
639
653
  f"dns-comms-{suffix}-{host.identifier}-domain",
640
654
  resource_group_name=self._rg,
641
655
  zone_name=host.zone.name,
@@ -643,8 +657,8 @@ class DjangoDeployment(pulumi.ComponentResource):
643
657
  record_type="TXT",
644
658
  ttl=3600,
645
659
  txt_records=[
646
- azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
647
- azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
660
+ azure.dns.TxtRecordArgs(value=[records["domain"]["value"]]),
661
+ azure.dns.TxtRecordArgs(value=[records["s_pf"]["value"]]),
648
662
  ],
649
663
  )
650
664
  created_records.append(r)
@@ -656,14 +670,14 @@ class DjangoDeployment(pulumi.ComponentResource):
656
670
  else:
657
671
  relative_record_set_name = f"{records[record]['name']}.{host.host}"
658
672
 
659
- r = azure.network.RecordSet(
673
+ r = azure.dns.RecordSet(
660
674
  f"dns-comms-{suffix}-{host.identifier}-{record}",
661
675
  resource_group_name=self._rg,
662
676
  zone_name=host.zone.name,
663
677
  relative_record_set_name=relative_record_set_name,
664
678
  record_type="CNAME",
665
679
  ttl=records[record]["ttl"],
666
- cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
680
+ cname_record=azure.dns.CnameRecordArgs(cname=records[record]["value"]),
667
681
  )
668
682
  created_records.append(r)
669
683
 
@@ -1026,7 +1040,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1026
1040
  if host.zone:
1027
1041
  # Create a DNS record in the zone.
1028
1042
  # We always use an A record instead of CNAME to avoid collisions with TXT records.
1029
- a = azure.network.RecordSet(
1043
+ a = azure.dns.RecordSet(
1030
1044
  f"dns-a-{name}-{self._name}-{host.identifier}",
1031
1045
  resource_group_name=self._rg,
1032
1046
  zone_name=host.zone.name,
@@ -1034,7 +1048,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1034
1048
  record_type="A",
1035
1049
  ttl=3600,
1036
1050
  a_records=[
1037
- azure.network.ARecordArgs(
1051
+ azure.dns.ARecordArgs(
1038
1052
  ipv4_address=virtual_ip,
1039
1053
  )
1040
1054
  ],
@@ -1045,7 +1059,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1045
1059
  # For the certificate validation to work
1046
1060
  relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
1047
1061
 
1048
- txt_validation = azure.network.RecordSet(
1062
+ txt_validation = azure.dns.RecordSet(
1049
1063
  f"dns-txt-{name}-{self._name}-{host.identifier}",
1050
1064
  resource_group_name=self._rg,
1051
1065
  zone_name=host.zone.name,
@@ -1053,7 +1067,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1053
1067
  record_type="TXT",
1054
1068
  ttl=3600,
1055
1069
  txt_records=[
1056
- azure.network.TxtRecordArgs(
1070
+ azure.dns.TxtRecordArgs(
1057
1071
  value=[app.custom_domain_verification_id],
1058
1072
  )
1059
1073
  ],
@@ -1126,7 +1140,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1126
1140
  # Grant the app access to the cache if needed.
1127
1141
  # We need to check explicitly if it is not None because the db could also be 0.
1128
1142
  if self._cache and cache_db is not None:
1129
- azure.cache.AccessPolicyAssignment(
1143
+ azure.redis.AccessPolicyAssignment(
1130
1144
  f"ra-{name}-cache",
1131
1145
  resource_group_name=self._rg,
1132
1146
  cache_name=self._cache.name,
@@ -1171,7 +1185,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1171
1185
 
1172
1186
  return app
1173
1187
 
1174
- def _strip_off_dns_zone_name(self, host: str, zone: azure.network.Zone) -> pulumi.Output[str]:
1188
+ def _strip_off_dns_zone_name(self, host: str, zone: azure.dns.Zone) -> pulumi.Output[str]:
1175
1189
  """
1176
1190
  Strip off the DNS zone name from the host name.
1177
1191
 
@@ -0,0 +1,15 @@
1
+ from django.core.cache import cache
2
+ from django.core.management.base import BaseCommand
3
+
4
+
5
+ class Command(BaseCommand):
6
+ help = "Purges the entire cache."
7
+
8
+ def handle(self, *args, **options):
9
+ self.stdout.write("Purging cache...")
10
+
11
+ try:
12
+ cache.clear()
13
+ self.stdout.write(self.style.SUCCESS("Successfully purged cache."))
14
+ except Exception as e:
15
+ self.stdout.write(self.style.ERROR(f"Failed to purge cache: {e}"))
@@ -4,6 +4,7 @@ from azure.mgmt.cdn import CdnManagementClient
4
4
  from azure.mgmt.cdn.models import PurgeParameters
5
5
  from django.conf import settings
6
6
  from django.core.management.base import BaseCommand
7
+
7
8
  from pulumi_django_azure.azure_helper import AZURE_CREDENTIAL
8
9
 
9
10
 
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
 
3
4
  from django.conf import settings
4
5
  from django.core.cache import cache
@@ -7,35 +8,57 @@ from django.db.utils import OperationalError
7
8
  from django.http import HttpResponse
8
9
  from django_redis import get_redis_connection
9
10
 
10
- from .azure_helper import get_db_password, get_redis_credentials
11
+ from .azure_helper import db_token_will_expire, get_db_password, get_redis_credentials, redis_token_will_expire
11
12
 
12
- logger = logging.getLogger(__name__)
13
+ logger = logging.getLogger("pulumi_django_azure.health_check")
13
14
 
14
15
 
15
16
  class HealthCheckMiddleware:
16
17
  def __init__(self, get_response):
17
18
  self.get_response = get_response
18
19
 
20
+ def _self_heal(self):
21
+ # Send HUP signal to gunicorn main thread,
22
+ # which will trigger new workers to start.
23
+ os.kill(os.getppid(), 1)
24
+
19
25
  def __call__(self, request):
20
26
  if request.path == settings.HEALTH_CHECK_PATH:
21
27
  # Update the database credentials if needed
22
28
  if settings.AZURE_DB_PASSWORD:
23
29
  try:
24
- settings.DATABASES["default"]["PASSWORD"] = get_db_password()
30
+ if db_token_will_expire():
31
+ logger.debug("Database token will expire, fetching new credentials")
32
+ settings.DATABASES["default"]["PASSWORD"] = get_db_password()
33
+ else:
34
+ logger.debug("Database token is still valid, skipping credentials update")
25
35
  except Exception as e:
26
36
  logger.error("Failed to update database credentials: %s", str(e))
37
+
38
+ self._self_heal()
39
+
27
40
  return HttpResponse(status=503)
28
41
 
29
42
  # Update the Redis credentials if needed
30
43
  if settings.AZURE_REDIS_CREDENTIALS:
31
44
  try:
32
- redis_credentials = get_redis_credentials()
33
- # Re-authenticate the Redis connection
34
- redis_connection = get_redis_connection("default")
35
- redis_connection.execute_command("AUTH", redis_credentials.username, redis_credentials.password)
36
- settings.CACHES["default"]["OPTIONS"]["PASSWORD"] = redis_credentials.password
45
+ if redis_token_will_expire():
46
+ logger.debug("Redis token will expire, fetching new credentials")
47
+
48
+ redis_credentials = get_redis_credentials()
49
+
50
+ # Re-authenticate the Redis connection
51
+ redis_connection = get_redis_connection("default")
52
+ redis_connection.execute_command("AUTH", redis_credentials.username, redis_credentials.password)
53
+
54
+ settings.CACHES["default"]["OPTIONS"]["PASSWORD"] = redis_credentials.password
55
+ else:
56
+ logger.debug("Redis token is still valid, skipping credentials update")
37
57
  except Exception as e:
38
58
  logger.error("Failed to update Redis credentials: %s", str(e))
59
+
60
+ self._self_heal()
61
+
39
62
  return HttpResponse(status=503)
40
63
 
41
64
  try:
@@ -102,14 +102,21 @@ if IS_AZURE_ENVIRONMENT:
102
102
  LOGGING = {
103
103
  "version": 1,
104
104
  "disable_existing_loggers": False,
105
+ "formatters": {
106
+ "timestamped": {
107
+ "format": "{asctime} {levelname} {message}",
108
+ "style": "{",
109
+ },
110
+ },
105
111
  "handlers": {
106
112
  "file": {
107
113
  "level": "INFO",
108
- "class": "logging.handlers.TimedRotatingFileHandler",
114
+ "class": "logging.handlers.RotatingFileHandler",
109
115
  "filename": "/home/LogFiles/django.log",
110
- "when": "h",
111
- "interval": 1,
112
- "backupCount": 24,
116
+ # 50 MB
117
+ "maxBytes": 52428800,
118
+ "backupCount": 5,
119
+ "formatter": "timestamped",
113
120
  },
114
121
  },
115
122
  "loggers": {
@@ -118,6 +125,11 @@ if IS_AZURE_ENVIRONMENT:
118
125
  "level": "INFO",
119
126
  "propagate": True,
120
127
  },
128
+ "pulumi_django_azure": {
129
+ "handlers": ["file"],
130
+ "level": "INFO",
131
+ "propagate": True,
132
+ },
121
133
  },
122
134
  }
123
135
 
@@ -1,52 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.26
3
+ Version: 1.0.28
4
4
  Summary: Simply deployment of Django on Azure with Pulumi
5
5
  Author-email: Maarten Ureel <maarten@youreal.eu>
6
- License: MIT License
7
-
8
- Copyright (c) 2023 YouReal BV
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Homepage, https://gitlab.com/MaartenUreel/pulumi-django-azure
29
8
  Keywords: django,pulumi,azure
30
- Classifier: License :: OSI Approved :: MIT License
31
9
  Classifier: Programming Language :: Python
32
10
  Classifier: Programming Language :: Python :: 3
33
11
  Requires-Python: <3.14,>=3.11
34
12
  Description-Content-Type: text/markdown
35
- License-File: LICENSE
36
13
  Requires-Dist: azure-identity<2.0.0,>=1.21.0
37
14
  Requires-Dist: azure-keyvault-secrets<5.0.0,>=4.9.0
38
15
  Requires-Dist: azure-mgmt-cdn<14.0.0,>=13.1.1
39
16
  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
17
+ Requires-Dist: django<6.0,>=5.2
18
+ Requires-Dist: django-azure-communication-email<2.0.0,>=1.3.2
42
19
  Requires-Dist: django-environ<0.13.0,>=0.12.0
43
20
  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
21
+ Requires-Dist: django-storages[azure]<2.0.0,>=1.14.6
22
+ Requires-Dist: pulumi>=3.165.0
23
+ Requires-Dist: pulumi-azure-native>=3.2.0
24
+ Requires-Dist: pulumi-random>=4.18.1
48
25
  Requires-Dist: redis[hiredis]<6.0.0,>=5.2.1
49
- Dynamic: license-file
50
26
 
51
27
  # Pulumi Django Deployment
52
28
 
@@ -76,7 +52,7 @@ Your Django project should contain a folder `cicd` with these files:
76
52
  sh cicd/collectstatic.sh &
77
53
 
78
54
  python manage.py migrate
79
- gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
55
+ gunicorn --timeout 600 --workers $((($NUM_CORES*2)+1)) --preload --chdir $APP_PATH yourapplication.wsgi --access-logfile '-' --error-logfile '-'
80
56
  ```
81
57
 
82
58
  Be sure to change `yourapplication` in the above.
@@ -168,7 +144,7 @@ django.add_database_administrator(
168
144
  ```python
169
145
  from pulumi_django_azure.settings import * # noqa: F403
170
146
 
171
- # This will provide the management command to purge the CDN
147
+ # This will provide the management command to purge the CDN and cache
172
148
  INSTALLED_APPS += ["pulumi_django_azure"]
173
149
 
174
150
  # This will provide the health check middleware that will also take care of credential rotation.
@@ -1,4 +1,3 @@
1
- LICENSE
2
1
  README.md
3
2
  pyproject.toml
4
3
  setup.cfg
@@ -13,4 +12,5 @@ src/pulumi_django_azure.egg-info/dependency_links.txt
13
12
  src/pulumi_django_azure.egg-info/requires.txt
14
13
  src/pulumi_django_azure.egg-info/top_level.txt
15
14
  src/pulumi_django_azure/management/commands/__init__.py
15
+ src/pulumi_django_azure/management/commands/purge_cache.py
16
16
  src/pulumi_django_azure/management/commands/purge_cdn.py
@@ -2,12 +2,12 @@ azure-identity<2.0.0,>=1.21.0
2
2
  azure-keyvault-secrets<5.0.0,>=4.9.0
3
3
  azure-mgmt-cdn<14.0.0,>=13.1.1
4
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
5
+ django<6.0,>=5.2
6
+ django-azure-communication-email<2.0.0,>=1.3.2
7
7
  django-environ<0.13.0,>=0.12.0
8
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
9
+ django-storages[azure]<2.0.0,>=1.14.6
10
+ pulumi>=3.165.0
11
+ pulumi-azure-native>=3.2.0
12
+ pulumi-random>=4.18.1
13
13
  redis[hiredis]<6.0.0,>=5.2.1
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 YouReal BV
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,67 +0,0 @@
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"]