pulumi-django-azure 1.0.25__tar.gz → 1.0.27__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.25/src/pulumi_django_azure.egg-info → pulumi_django_azure-1.0.27}/PKG-INFO +9 -33
  2. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/README.md +2 -2
  3. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/pyproject.toml +19 -19
  4. pulumi_django_azure-1.0.27/src/pulumi_django_azure/azure_helper.py +114 -0
  5. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure/django_deployment.py +34 -34
  6. pulumi_django_azure-1.0.27/src/pulumi_django_azure/management/commands/purge_cache.py +15 -0
  7. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure/middleware.py +31 -8
  8. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure/settings.py +18 -4
  9. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27/src/pulumi_django_azure.egg-info}/PKG-INFO +9 -33
  10. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure.egg-info/SOURCES.txt +1 -1
  11. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure.egg-info/requires.txt +5 -5
  12. pulumi_django_azure-1.0.25/LICENSE +0 -21
  13. pulumi_django_azure-1.0.25/src/pulumi_django_azure/azure_helper.py +0 -67
  14. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/setup.cfg +0 -0
  15. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure/__init__.py +0 -0
  16. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure/management/commands/__init__.py +0 -0
  17. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure/management/commands/purge_cdn.py +0 -0
  18. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/src/pulumi_django_azure.egg-info/dependency_links.txt +0 -0
  19. {pulumi_django_azure-1.0.25 → pulumi_django_azure-1.0.27}/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.25
3
+ Version: 1.0.27
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
21
+ Requires-Dist: django-storages[azure]<2.0.0,>=1.14.6
22
+ Requires-Dist: pulumi>=3.163.0
23
+ Requires-Dist: pulumi-azure-native>=3.2.0
47
24
  Requires-Dist: pulumi-random>=4.18.0
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.25"
7
+ version = "1.0.27"
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)",
26
+ "django-storages[azure] (>=1.14.6,<2.0.0)",
27
+ "pulumi (>=3.163.0)",
28
+ "pulumi-azure-native (>=3.2.0)",
30
29
  "pulumi-random (>=4.18.0)",
31
- "redis[hiredis] (>=5.2.1,<6.0.0)",
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.25"
41
- description = "Simply deployment of Django on Azure with Pulumi"
39
+ version = "1.0.27"
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.163.0"
55
+ pulumi-azure-native = ">=3.2.0"
56
+ pulumi-random = ">=4.18.0"
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.6"
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
@@ -69,9 +69,9 @@ class DjangoDeployment(pulumi.ComponentResource):
69
69
  storage_account_name: str,
70
70
  storage_allowed_origins: Sequence[str] | None = None,
71
71
  pgadmin_access_ip: Sequence[str] | None = None,
72
- pgadmin_dns_zone: azure.network.Zone | None = None,
72
+ pgadmin_dns_zone: azure.dns.Zone | None = None,
73
73
  cache_ip_prefix: str | None = None,
74
- cache_sku: azure.cache.SkuArgs | None = None,
74
+ cache_sku: azure.redis.SkuArgs | None = None,
75
75
  cdn_host: HostDefinition | None = None,
76
76
  opts=None,
77
77
  ):
@@ -220,14 +220,14 @@ class DjangoDeployment(pulumi.ComponentResource):
220
220
  if custom_host:
221
221
  if custom_host.zone:
222
222
  # Create a DNS record for the custom host in the given zone
223
- rs = azure.network.RecordSet(
223
+ rs = azure.dns.RecordSet(
224
224
  f"cdn-cname-{self._name}",
225
225
  resource_group_name=self._rg,
226
226
  zone_name=custom_host.zone.name,
227
227
  relative_record_set_name=custom_host.host,
228
228
  record_type="CNAME",
229
229
  ttl=3600,
230
- target_resource=azure.network.SubResourceArgs(
230
+ target_resource=azure.dns.SubResourceArgs(
231
231
  id=self._cdn_endpoint.id,
232
232
  ),
233
233
  )
@@ -266,7 +266,7 @@ class DjangoDeployment(pulumi.ComponentResource):
266
266
  )
267
267
 
268
268
  # Create private DNS zone
269
- dns = azure.network.PrivateZone(
269
+ dns = azure.privatedns.PrivateZone(
270
270
  f"dns-pgsql-{self._name}",
271
271
  resource_group_name=self._rg,
272
272
  location="global",
@@ -275,7 +275,7 @@ class DjangoDeployment(pulumi.ComponentResource):
275
275
  )
276
276
 
277
277
  # Link the private DNS zone to the VNet in order to make resolving work
278
- azure.network.VirtualNetworkLink(
278
+ azure.privatedns.VirtualNetworkLink(
279
279
  f"vnet-link-pgsql-{self._name}",
280
280
  resource_group_name=self._rg,
281
281
  location="global",
@@ -308,23 +308,23 @@ class DjangoDeployment(pulumi.ComponentResource):
308
308
 
309
309
  pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
310
310
 
311
- def _create_cache(self, sku: azure.cache.SkuArgs, ip_prefix: str):
311
+ def _create_cache(self, sku: azure.redis.SkuArgs, ip_prefix: str):
312
312
  # Create a Redis cache
313
- self._cache = azure.cache.Redis(
313
+ self._cache = azure.redis.Redis(
314
314
  f"cache-{self._name}",
315
315
  resource_group_name=self._rg,
316
316
  sku=sku,
317
317
  enable_non_ssl_port=False,
318
- public_network_access=azure.cache.PublicNetworkAccess.DISABLED,
318
+ public_network_access=azure.redis.PublicNetworkAccess.DISABLED,
319
319
  )
320
320
 
321
321
  # Create an access policy that gives us access to the cache
322
- self._cache_access_policy = azure.cache.AccessPolicy(
322
+ self._cache_access_policy = azure.redis.AccessPolicy(
323
323
  f"cache-access-policy-{self._name}",
324
324
  resource_group_name=self._rg,
325
325
  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",
326
+ # Same as the built in Data Contributor policy + flushdb permissions
327
+ permissions="+@all -@dangerous +flushdb +cluster|info +cluster|nodes +cluster|slots allkeys",
328
328
  )
329
329
 
330
330
  # Allocate a subnet for the cache
@@ -334,7 +334,7 @@ class DjangoDeployment(pulumi.ComponentResource):
334
334
  )
335
335
 
336
336
  # Create a private DNS zone for the cache
337
- dns = azure.network.PrivateZone(
337
+ dns = azure.privatedns.PrivateZone(
338
338
  f"dns-cache-{self._name}",
339
339
  resource_group_name=self._rg,
340
340
  location="global",
@@ -342,7 +342,7 @@ class DjangoDeployment(pulumi.ComponentResource):
342
342
  )
343
343
 
344
344
  # Link the private DNS zone to the VNet in order to make resolving work
345
- azure.network.VirtualNetworkLink(
345
+ azure.privatedns.VirtualNetworkLink(
346
346
  f"vnet-link-cache-{self._name}",
347
347
  resource_group_name=self._rg,
348
348
  location="global",
@@ -377,14 +377,14 @@ class DjangoDeployment(pulumi.ComponentResource):
377
377
  )
378
378
 
379
379
  # Create a DNS record for the cache
380
- azure.network.PrivateRecordSet(
380
+ azure.privatedns.PrivateRecordSet(
381
381
  f"dns-a-cache-{self._name}",
382
382
  resource_group_name=self._rg,
383
383
  private_zone_name=dns.name,
384
384
  relative_record_set_name=self._cache.name,
385
385
  record_type="A",
386
386
  ttl=300,
387
- a_records=[azure.network.ARecordArgs(ipv4_address=ip)],
387
+ a_records=[azure.privatedns.ARecordArgs(ipv4_address=ip)],
388
388
  )
389
389
 
390
390
  def _create_subnet(
@@ -432,7 +432,7 @@ class DjangoDeployment(pulumi.ComponentResource):
432
432
  sku=sku,
433
433
  )
434
434
 
435
- def _create_pgadmin_app(self, access_ip: Sequence[str] | None = None, dns_zone: azure.network.Zone | None = None):
435
+ def _create_pgadmin_app(self, access_ip: Sequence[str] | None = None, dns_zone: azure.dns.Zone | None = None):
436
436
  # Determine the IP restrictions
437
437
  ip_restrictions = []
438
438
  default_restriction = azure.web.DefaultAction.ALLOW
@@ -490,20 +490,20 @@ class DjangoDeployment(pulumi.ComponentResource):
490
490
 
491
491
  if dns_zone:
492
492
  # Create a DNS record for the pgAdmin app
493
- cname = azure.network.RecordSet(
493
+ cname = azure.dns.RecordSet(
494
494
  f"dns-cname-pgadmin-{self._name}",
495
495
  resource_group_name=self._rg,
496
496
  zone_name=dns_zone.name,
497
497
  relative_record_set_name="pgadmin",
498
498
  record_type="CNAME",
499
499
  ttl=3600,
500
- cname_record=azure.network.CnameRecordArgs(
500
+ cname_record=azure.dns.CnameRecordArgs(
501
501
  cname=app.default_host_name,
502
502
  ),
503
503
  )
504
504
 
505
505
  # For the certificate validation to work
506
- txt_validation = azure.network.RecordSet(
506
+ txt_validation = azure.dns.RecordSet(
507
507
  f"dns-txt-pgadmin-{self._name}",
508
508
  resource_group_name=self._rg,
509
509
  zone_name=dns_zone.name,
@@ -511,7 +511,7 @@ class DjangoDeployment(pulumi.ComponentResource):
511
511
  record_type="TXT",
512
512
  ttl=3600,
513
513
  txt_records=[
514
- azure.network.TxtRecordArgs(
514
+ azure.dns.TxtRecordArgs(
515
515
  value=[app.custom_domain_verification_id],
516
516
  )
517
517
  ],
@@ -631,11 +631,11 @@ class DjangoDeployment(pulumi.ComponentResource):
631
631
 
632
632
  existing_binding.apply(_create_binding_with_cert)
633
633
 
634
- def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
634
+ def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.dns.RecordSet]:
635
635
  created_records = []
636
636
 
637
637
  # Domain validation and SPF record (one TXT record with multiple values)
638
- r = azure.network.RecordSet(
638
+ r = azure.dns.RecordSet(
639
639
  f"dns-comms-{suffix}-{host.identifier}-domain",
640
640
  resource_group_name=self._rg,
641
641
  zone_name=host.zone.name,
@@ -643,8 +643,8 @@ class DjangoDeployment(pulumi.ComponentResource):
643
643
  record_type="TXT",
644
644
  ttl=3600,
645
645
  txt_records=[
646
- azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
647
- azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
646
+ azure.dns.TxtRecordArgs(value=[records["domain"]["value"]]),
647
+ azure.dns.TxtRecordArgs(value=[records["s_pf"]["value"]]),
648
648
  ],
649
649
  )
650
650
  created_records.append(r)
@@ -656,14 +656,14 @@ class DjangoDeployment(pulumi.ComponentResource):
656
656
  else:
657
657
  relative_record_set_name = f"{records[record]['name']}.{host.host}"
658
658
 
659
- r = azure.network.RecordSet(
659
+ r = azure.dns.RecordSet(
660
660
  f"dns-comms-{suffix}-{host.identifier}-{record}",
661
661
  resource_group_name=self._rg,
662
662
  zone_name=host.zone.name,
663
663
  relative_record_set_name=relative_record_set_name,
664
664
  record_type="CNAME",
665
665
  ttl=records[record]["ttl"],
666
- cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
666
+ cname_record=azure.dns.CnameRecordArgs(cname=records[record]["value"]),
667
667
  )
668
668
  created_records.append(r)
669
669
 
@@ -1026,7 +1026,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1026
1026
  if host.zone:
1027
1027
  # Create a DNS record in the zone.
1028
1028
  # We always use an A record instead of CNAME to avoid collisions with TXT records.
1029
- a = azure.network.RecordSet(
1029
+ a = azure.dns.RecordSet(
1030
1030
  f"dns-a-{name}-{self._name}-{host.identifier}",
1031
1031
  resource_group_name=self._rg,
1032
1032
  zone_name=host.zone.name,
@@ -1034,7 +1034,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1034
1034
  record_type="A",
1035
1035
  ttl=3600,
1036
1036
  a_records=[
1037
- azure.network.ARecordArgs(
1037
+ azure.dns.ARecordArgs(
1038
1038
  ipv4_address=virtual_ip,
1039
1039
  )
1040
1040
  ],
@@ -1045,7 +1045,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1045
1045
  # For the certificate validation to work
1046
1046
  relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
1047
1047
 
1048
- txt_validation = azure.network.RecordSet(
1048
+ txt_validation = azure.dns.RecordSet(
1049
1049
  f"dns-txt-{name}-{self._name}-{host.identifier}",
1050
1050
  resource_group_name=self._rg,
1051
1051
  zone_name=host.zone.name,
@@ -1053,7 +1053,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1053
1053
  record_type="TXT",
1054
1054
  ttl=3600,
1055
1055
  txt_records=[
1056
- azure.network.TxtRecordArgs(
1056
+ azure.dns.TxtRecordArgs(
1057
1057
  value=[app.custom_domain_verification_id],
1058
1058
  )
1059
1059
  ],
@@ -1126,7 +1126,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1126
1126
  # Grant the app access to the cache if needed.
1127
1127
  # We need to check explicitly if it is not None because the db could also be 0.
1128
1128
  if self._cache and cache_db is not None:
1129
- azure.cache.AccessPolicyAssignment(
1129
+ azure.redis.AccessPolicyAssignment(
1130
1130
  f"ra-{name}-cache",
1131
1131
  resource_group_name=self._rg,
1132
1132
  cache_name=self._cache.name,
@@ -1171,7 +1171,7 @@ class DjangoDeployment(pulumi.ComponentResource):
1171
1171
 
1172
1172
  return app
1173
1173
 
1174
- def _strip_off_dns_zone_name(self, host: str, zone: azure.network.Zone) -> pulumi.Output[str]:
1174
+ def _strip_off_dns_zone_name(self, host: str, zone: azure.dns.Zone) -> pulumi.Output[str]:
1175
1175
  """
1176
1176
  Strip off the DNS zone name from the host name.
1177
1177
 
@@ -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}"))
@@ -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
 
@@ -152,3 +164,5 @@ if redis_cache_host and redis_cache_port and redis_cache_db:
152
164
  },
153
165
  },
154
166
  }
167
+ else:
168
+ AZURE_REDIS_CREDENTIALS = False
@@ -1,52 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.25
3
+ Version: 1.0.27
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
21
+ Requires-Dist: django-storages[azure]<2.0.0,>=1.14.6
22
+ Requires-Dist: pulumi>=3.163.0
23
+ Requires-Dist: pulumi-azure-native>=3.2.0
47
24
  Requires-Dist: pulumi-random>=4.18.0
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
9
+ django-storages[azure]<2.0.0,>=1.14.6
10
+ pulumi>=3.163.0
11
+ pulumi-azure-native>=3.2.0
12
12
  pulumi-random>=4.18.0
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"]