pulumi-django-azure 1.0.12__tar.gz → 1.0.14__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.12
3
+ Version: 1.0.14
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pulumi-django-azure"
7
- version = "1.0.12"
7
+ version = "1.0.14"
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" }]
@@ -27,19 +27,36 @@ Homepage = "https://gitlab.com/MaartenUreel/pulumi-django-azure"
27
27
 
28
28
  [tool.poetry]
29
29
  name = "pulumi-django-azure"
30
- version = "1.0.12"
30
+ version = "1.0.14"
31
31
  description = "Simply deployment of Django on Azure with Pulumi"
32
32
  authors = ["Maarten Ureel <maarten@youreal.eu>"]
33
33
 
34
34
  [tool.poetry.dependencies]
35
35
  python = "^3.11"
36
- pulumi-azure-native = "^2.24.0"
37
- pulumi = "^3.99.0"
38
- pulumi-random = "^4.15.0"
36
+ pulumi-azure-native = "^2.64.2"
37
+ pulumi = "^3.135.0"
38
+ pulumi-random = "^4.16.6"
39
39
 
40
40
  [tool.poetry.group.dev.dependencies]
41
- twine = "^4.0.2"
42
- build = "^1.0.3"
41
+ twine = "^5.1.1"
42
+ build = "^1.2.2"
43
+ ruff = "^0.4.9"
43
44
 
44
- [tool.isort]
45
- profile = "black"
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
+ ]
@@ -0,0 +1 @@
1
+ from .django_deployment import DjangoDeployment, HostDefinition # noqa: F401
@@ -1,10 +1,56 @@
1
- from typing import Optional, Sequence
1
+ from collections.abc import Sequence
2
+ from typing import Optional, Union
2
3
 
3
4
  import pulumi
4
5
  import pulumi_azure_native as azure
5
6
  import pulumi_random
6
7
 
7
8
 
9
+ class HostDefinition:
10
+ """
11
+ A definition for a custom host name, optionally with a DNS zone.
12
+
13
+ :param host: The host name. If a zone is given, this is the relative host name.
14
+ :param zone: The DNS zone (optional).
15
+ :param identifier: An identifier for this host definition (optional).
16
+ """
17
+
18
+ def __init__(self, host: str, zone: Optional[azure.network.Zone] = None, identifier: Optional[str] = None):
19
+ self.host = host
20
+ self.zone = zone
21
+ self._identifier = identifier
22
+
23
+ @property
24
+ def identifier(self) -> str:
25
+ """
26
+ The identifier for this host definition.
27
+
28
+ :return: The identifier
29
+ """
30
+ if not self._identifier:
31
+ if self.zone:
32
+ raise ValueError(f"An identifier is required for the HostDefinition with host '{self.host}' ensure uniqueness.")
33
+ else:
34
+ # Use the host name as the identifier
35
+ return self.host.replace(".", "-")
36
+ else:
37
+ return self._identifier
38
+
39
+ @property
40
+ def full_host(self) -> pulumi.Output[str]:
41
+ """
42
+ The full host name, including the zone.
43
+
44
+ :return: The full host name
45
+ """
46
+ if not self.zone:
47
+ return pulumi.Output.concat(self.host)
48
+ elif self.host == "@":
49
+ return self.zone.name
50
+ else:
51
+ return pulumi.Output.concat(self.host, ".", self.zone.name)
52
+
53
+
8
54
  class DjangoDeployment(pulumi.ComponentResource):
9
55
  HEALTH_CHECK_PATH = "/health-check"
10
56
 
@@ -19,7 +65,9 @@ class DjangoDeployment(pulumi.ComponentResource):
19
65
  appservice_ip_prefix: str,
20
66
  app_service_sku: azure.web.SkuDescriptionArgs,
21
67
  storage_account_name: str,
22
- cdn_host: Optional[str],
68
+ pgadmin_access_ip: Optional[Sequence[str]] = None,
69
+ pgadmin_dns_zone: Optional[azure.network.Zone] = None,
70
+ cdn_host: Optional[HostDefinition] = None,
23
71
  opts=None,
24
72
  ):
25
73
  """
@@ -34,7 +82,9 @@ class DjangoDeployment(pulumi.ComponentResource):
34
82
  :param appservice_ip_prefix: The IP prefix for the app service subnet.
35
83
  :param app_service_sku: The SKU for the app service plan.
36
84
  :param storage_account_name: The name of the storage account. Should be unique across Azure.
37
- :param cdn_host: A custom CDN host name (optional)
85
+ :param pgadmin_access_ip: The IP addresses to allow access to pgAdmin. If empty, all IP addresses are allowed.
86
+ :param pgadmin_dns_zone: The Azure DNS zone to a pgadmin DNS record in. (optional)
87
+ :param cdn_host: A custom CDN host name. (optional)
38
88
  :param opts: The resource options
39
89
  """
40
90
 
@@ -67,7 +117,7 @@ class DjangoDeployment(pulumi.ComponentResource):
67
117
  self._app_service_plan = self._create_app_service_plan(sku=app_service_sku)
68
118
 
69
119
  # Create a pgAdmin app
70
- self._create_pgadmin_app()
120
+ self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
71
121
 
72
122
  def _create_storage(self, account_name: str):
73
123
  # Create blob storage
@@ -85,7 +135,7 @@ class DjangoDeployment(pulumi.ComponentResource):
85
135
  enable_https_traffic_only=True,
86
136
  )
87
137
 
88
- def _create_cdn(self, custom_host: Optional[str]) -> pulumi.Output[str]:
138
+ def _create_cdn(self, custom_host: Optional[HostDefinition]) -> pulumi.Output[str]:
89
139
  """
90
140
  Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
91
141
  Otherwise, the default CDN host name will be returned.
@@ -135,15 +185,41 @@ class DjangoDeployment(pulumi.ComponentResource):
135
185
 
136
186
  # Add custom domain if given
137
187
  if custom_host:
138
- azure.cdn.CustomDomain(
139
- f"cdn-custom-domain-{self._name}",
140
- resource_group_name=self._rg,
141
- profile_name=self._cdn_profile.name,
142
- endpoint_name=self._cdn_endpoint.name,
143
- host_name=custom_host,
144
- )
188
+ if custom_host.zone:
189
+ # Create a DNS record for the custom host in the given zone
190
+ rs = azure.network.RecordSet(
191
+ f"cdn-cname-{self._name}",
192
+ resource_group_name=self._rg,
193
+ zone_name=custom_host.zone.name,
194
+ relative_record_set_name=custom_host.host,
195
+ record_type="CNAME",
196
+ ttl=3600,
197
+ target_resource=azure.network.SubResourceArgs(
198
+ id=self._cdn_endpoint.id,
199
+ ),
200
+ )
201
+
202
+ azure.cdn.CustomDomain(
203
+ f"cdn-custom-domain-{self._name}",
204
+ resource_group_name=self._rg,
205
+ profile_name=self._cdn_profile.name,
206
+ endpoint_name=self._cdn_endpoint.name,
207
+ host_name=custom_host.full_host,
208
+ opts=pulumi.ResourceOptions(depends_on=rs),
209
+ )
145
210
 
146
- return custom_host
211
+ return custom_host.full_host
212
+ else:
213
+ # Add custom hostname without a zone
214
+ azure.cdn.CustomDomain(
215
+ f"cdn-custom-domain-{self._name}",
216
+ resource_group_name=self._rg,
217
+ profile_name=self._cdn_profile.name,
218
+ endpoint_name=self._cdn_endpoint.name,
219
+ host_name=custom_host.host,
220
+ )
221
+
222
+ return custom_host.host
147
223
  else:
148
224
  # Return the default CDN host name
149
225
  return self._cdn_endpoint.host_name
@@ -200,7 +276,11 @@ class DjangoDeployment(pulumi.ComponentResource):
200
276
  pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
201
277
 
202
278
  def _create_subnet(
203
- self, name, prefix, delegation_service: Optional[str] = None, service_endpoints: Sequence[str] = []
279
+ self,
280
+ name,
281
+ prefix,
282
+ delegation_service: Optional[str] = None,
283
+ service_endpoints: Sequence[str] = [],
204
284
  ) -> azure.network.Subnet:
205
285
  """
206
286
  Generic method to create a subnet with a delegation.
@@ -239,7 +319,22 @@ class DjangoDeployment(pulumi.ComponentResource):
239
319
  sku=sku,
240
320
  )
241
321
 
242
- def _create_pgadmin_app(self):
322
+ def _create_pgadmin_app(self, access_ip: Optional[Sequence[str]] = None, dns_zone: Optional[azure.network.Zone] = None):
323
+ # Determine the IP restrictions
324
+ ip_restrictions = []
325
+ default_restriction = azure.web.DefaultAction.ALLOW
326
+ if access_ip:
327
+ default_restriction = azure.web.DefaultAction.DENY
328
+
329
+ for ip in access_ip:
330
+ ip_restrictions.append(
331
+ azure.web.IpSecurityRestrictionArgs(
332
+ action="Allow",
333
+ ip_address=ip,
334
+ priority=300,
335
+ )
336
+ )
337
+
243
338
  # The app itself
244
339
  app = azure.web.WebApp(
245
340
  f"app-pgadmin-{self._name}",
@@ -255,7 +350,10 @@ class DjangoDeployment(pulumi.ComponentResource):
255
350
  linux_fx_version="DOCKER|dpage/pgadmin4",
256
351
  health_check_path="/misc/ping",
257
352
  app_settings=[
258
- azure.web.NameValuePairArgs(name="DOCKER_REGISTRY_SERVER_URL", value="https://index.docker.io/v1"),
353
+ azure.web.NameValuePairArgs(
354
+ name="DOCKER_REGISTRY_SERVER_URL",
355
+ value="https://index.docker.io/v1",
356
+ ),
259
357
  azure.web.NameValuePairArgs(name="DOCKER_ENABLE_CI", value="true"),
260
358
  # azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
261
359
  # pgAdmin settings
@@ -263,6 +361,9 @@ class DjangoDeployment(pulumi.ComponentResource):
263
361
  azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
264
362
  azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
265
363
  ],
364
+ # IP restrictions
365
+ ip_security_restrictions_default_action=default_restriction,
366
+ ip_security_restrictions=ip_restrictions,
266
367
  ),
267
368
  )
268
369
 
@@ -274,6 +375,50 @@ class DjangoDeployment(pulumi.ComponentResource):
274
375
  share_name="pgadmin",
275
376
  )
276
377
 
378
+ if dns_zone:
379
+ # Create a DNS record for the pgAdmin app
380
+ cname = azure.network.RecordSet(
381
+ f"dns-cname-pgadmin-{self._name}",
382
+ resource_group_name=self._rg,
383
+ zone_name=dns_zone.name,
384
+ relative_record_set_name="pgadmin",
385
+ record_type="CNAME",
386
+ ttl=3600,
387
+ cname_record=azure.network.CnameRecordArgs(
388
+ cname=app.default_host_name,
389
+ ),
390
+ )
391
+
392
+ # For the certificate validation to work
393
+ txt_validation = azure.network.RecordSet(
394
+ f"dns-txt-pgadmin-{self._name}",
395
+ resource_group_name=self._rg,
396
+ zone_name=dns_zone.name,
397
+ relative_record_set_name="asuid.pgadmin",
398
+ record_type="TXT",
399
+ ttl=3600,
400
+ txt_records=[
401
+ azure.network.TxtRecordArgs(
402
+ value=[app.custom_domain_verification_id],
403
+ )
404
+ ],
405
+ )
406
+
407
+ # Add custom hostname
408
+ self._add_webapp_host(
409
+ app=app,
410
+ host=dns_zone.name.apply(lambda name: f"pgadmin.{name}"),
411
+ suffix=self._name,
412
+ depends_on=[cname, txt_validation],
413
+ identifier="pgadmin",
414
+ )
415
+
416
+ # Export the custom hostname
417
+ pulumi.export("pgadmin_url", dns_zone.name.apply(lambda name: f"https://pgadmin.{name}"))
418
+ else:
419
+ # Export the default hostname
420
+ pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
421
+
277
422
  # Mount the storage container
278
423
  azure.web.WebAppAzureStorageAccounts(
279
424
  f"app-pgadmin-mount-{self._name}",
@@ -290,9 +435,24 @@ class DjangoDeployment(pulumi.ComponentResource):
290
435
  },
291
436
  )
292
437
 
293
- pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
438
+ def _get_existing_web_app_host_name_binding(self, resource_group_name: str, app_name: str, host_name: str):
439
+ try:
440
+ return azure.web.get_web_app_host_name_binding(
441
+ resource_group_name=resource_group_name,
442
+ name=app_name,
443
+ host_name=host_name,
444
+ )
445
+ except Exception:
446
+ return None
294
447
 
295
- def _add_webapp_host(self, app: azure.web.WebApp, host: str, suffix: str):
448
+ def _add_webapp_host(
449
+ self,
450
+ app: azure.web.WebApp,
451
+ host: Union[str, pulumi.Input[str]],
452
+ suffix: str,
453
+ identifier: str,
454
+ depends_on: Optional[Sequence[pulumi.Resource]] = None,
455
+ ):
296
456
  """
297
457
  Because of a circular dependency, we need to create the certificate and the binding in two steps.
298
458
  First we create a binding without a certificate,
@@ -305,48 +465,93 @@ class DjangoDeployment(pulumi.ComponentResource):
305
465
  :param app: The web app
306
466
  :param host: The host name
307
467
  :param suffix: A suffix to make the resource name unique
468
+ :param depend_on: The resource to depend on (optional)
308
469
  """
309
470
 
310
- safe_host = host.replace(".", "-")
471
+ if not depends_on:
472
+ depends_on = []
311
473
 
312
- try:
313
- # Retrieve the existing binding - this will throw an exception if it doesn't exist
314
- azure.web.get_web_app_host_name_binding(
315
- resource_group_name=app.resource_group,
316
- name=app.name,
317
- host_name=host,
474
+ # Retrieve the existing binding (None if it doesn't exist)
475
+ existing_binding = pulumi.Output.all(app.resource_group, app.name, host).apply(
476
+ lambda args: self._get_existing_web_app_host_name_binding(
477
+ resource_group_name=args[0],
478
+ app_name=args[1],
479
+ host_name=args[2],
318
480
  )
481
+ )
319
482
 
320
- # Create a managed certificate
321
- # This will work because the binding exists actually
322
- certificate = azure.web.Certificate(
323
- f"cert-{suffix}-{safe_host}",
324
- resource_group_name=self._rg,
325
- server_farm_id=app.server_farm_id,
326
- canonical_name=host,
327
- host_names=[host],
328
- )
483
+ # Create an inline function that we will invoke through the Output.apply lambda
484
+ def _create_binding_with_cert(existing_binding):
485
+ if existing_binding:
486
+ # Create a managed certificate
487
+ # This will work because the binding exists actually
488
+ certificate = azure.web.Certificate(
489
+ f"cert-{suffix}-{identifier}",
490
+ resource_group_name=self._rg,
491
+ server_farm_id=app.server_farm_id,
492
+ canonical_name=host,
493
+ host_names=[host],
494
+ opts=pulumi.ResourceOptions(depends_on=depends_on),
495
+ )
329
496
 
330
- # Create a new binding, replacing the old one,
331
- # with the certificate
332
- azure.web.WebAppHostNameBinding(
333
- f"host-binding-{suffix}-{safe_host}",
334
- resource_group_name=self._rg,
335
- name=app.name,
336
- host_name=host,
337
- ssl_state=azure.web.SslState.SNI_ENABLED,
338
- thumbprint=certificate.thumbprint,
339
- )
340
- except Exception:
341
- # Create a binding without a certificate
342
- azure.web.WebAppHostNameBinding(
343
- f"host-binding-{suffix}-{safe_host}",
497
+ # Create a new binding, replacing the old one,
498
+ # with the certificate
499
+ azure.web.WebAppHostNameBinding(
500
+ f"host-binding-{suffix}-{identifier}",
501
+ resource_group_name=self._rg,
502
+ name=app.name,
503
+ host_name=host,
504
+ ssl_state=azure.web.SslState.SNI_ENABLED,
505
+ thumbprint=certificate.thumbprint,
506
+ opts=pulumi.ResourceOptions(depends_on=depends_on),
507
+ )
508
+
509
+ else:
510
+ # Create a binding without a certificate
511
+ azure.web.WebAppHostNameBinding(
512
+ f"host-binding-{suffix}-{identifier}",
513
+ resource_group_name=self._rg,
514
+ name=app.name,
515
+ host_name=host,
516
+ opts=pulumi.ResourceOptions(depends_on=depends_on),
517
+ )
518
+
519
+ existing_binding.apply(_create_binding_with_cert)
520
+
521
+ def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
522
+ created_records = []
523
+
524
+ # Domain validation and SPF record (one TXT record with multiple values)
525
+ r = azure.network.RecordSet(
526
+ f"dns-comms-{suffix}-{host.identifier}-domain",
527
+ resource_group_name=self._rg,
528
+ zone_name=host.zone.name,
529
+ relative_record_set_name="@",
530
+ record_type="TXT",
531
+ ttl=3600,
532
+ txt_records=[
533
+ azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
534
+ azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
535
+ ],
536
+ )
537
+ created_records.append(r)
538
+
539
+ # DKIM records (two CNAME records)
540
+ for record in ("d_kim", "d_kim2"):
541
+ r = azure.network.RecordSet(
542
+ f"dns-comms-{suffix}-{host.identifier}-{record}",
344
543
  resource_group_name=self._rg,
345
- name=app.name,
346
- host_name=host,
544
+ zone_name=host.zone.name,
545
+ relative_record_set_name=records[record]["name"],
546
+ record_type="CNAME",
547
+ ttl=records[record]["ttl"],
548
+ cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
347
549
  )
550
+ created_records.append(r)
348
551
 
349
- def _add_webapp_comms(self, data_location: str, domains: list[str], suffix: str) -> azure.communication.CommunicationService:
552
+ return created_records
553
+
554
+ def _add_webapp_comms(self, data_location: str, domains: list[HostDefinition], suffix: str) -> azure.communication.CommunicationService:
350
555
  email_service = azure.communication.EmailService(
351
556
  f"comms-email-{suffix}",
352
557
  resource_group_name=self._rg,
@@ -355,30 +560,37 @@ class DjangoDeployment(pulumi.ComponentResource):
355
560
  )
356
561
 
357
562
  domain_resources = []
358
- if domains:
359
- # Add our own custom domains
360
- for domain in domains:
361
- safe_host = domain.replace(".", "-")
362
- d = azure.communication.Domain(
363
- f"comms-email-domain-{suffix}-{safe_host}",
364
- resource_group_name=self._rg,
365
- location="global",
366
- domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
367
- domain_name=domain,
368
- email_service_name=email_service.name,
369
- )
370
- domain_resources.append(d.id.apply(lambda n: n))
371
- else:
372
- # Add an Azure managed domain
563
+ comm_dependencies = []
564
+
565
+ # Add our own custom domains
566
+ for domain in domains:
373
567
  d = azure.communication.Domain(
374
- f"comms-email-domain-{suffix}-azure",
568
+ f"comms-email-domain-{suffix}-{domain.identifier}",
375
569
  resource_group_name=self._rg,
376
570
  location="global",
377
- domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
378
- domain_name="AzureManagedDomain",
571
+ domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
572
+ domain_name=domain.full_host,
379
573
  email_service_name=email_service.name,
380
574
  )
381
- domain_resources.append(d.id.apply(lambda n: n))
575
+
576
+ if domain.zone:
577
+ # Create DNS records in the managed zone
578
+ comm_dependencies = pulumi.Output.all(suffix, domain, d.verification_records).apply(
579
+ lambda args: self._create_comms_dns_records(suffix=args[0], host=args[1], records=args[2])
580
+ )
581
+
582
+ domain_resources.append(d.id)
583
+
584
+ # Add an Azure managed domain
585
+ d = azure.communication.Domain(
586
+ f"comms-email-domain-{suffix}-azure",
587
+ resource_group_name=self._rg,
588
+ location="global",
589
+ domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
590
+ domain_name="AzureManagedDomain",
591
+ email_service_name=email_service.name,
592
+ )
593
+ domain_resources.append(d.id)
382
594
 
383
595
  # Create Communication Services and link the domains
384
596
  comm_service = azure.communication.CommunicationService(
@@ -387,6 +599,7 @@ class DjangoDeployment(pulumi.ComponentResource):
387
599
  location="global",
388
600
  data_location=data_location,
389
601
  linked_domains=domain_resources,
602
+ opts=pulumi.ResourceOptions(depends_on=comm_dependencies),
390
603
  )
391
604
 
392
605
  return comm_service
@@ -415,27 +628,35 @@ class DjangoDeployment(pulumi.ComponentResource):
415
628
  ),
416
629
  )
417
630
 
418
- # Find the Key Vault Administrator role
419
- administrator_role = vault.id.apply(
420
- lambda scope: azure.authorization.get_role_definition(
421
- role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
422
- scope=scope,
423
- )
424
- )
425
-
426
631
  # Add vault administrators
427
- for a in administrators:
428
- azure.authorization.RoleAssignment(
429
- f"ra-{suffix}-vault-admin-{a}",
430
- principal_id=a,
431
- principal_type=azure.authorization.PrincipalType.USER,
432
- role_definition_id=administrator_role.id,
433
- scope=vault.id,
632
+ if administrators:
633
+ # Find the Key Vault Administrator role
634
+ administrator_role = vault.id.apply(
635
+ lambda scope: azure.authorization.get_role_definition(
636
+ role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
637
+ scope=scope,
638
+ )
434
639
  )
435
640
 
641
+ # Actual administrator roles
642
+ for a in administrators:
643
+ azure.authorization.RoleAssignment(
644
+ f"ra-{suffix}-vault-admin-{a}",
645
+ principal_id=a,
646
+ principal_type=azure.authorization.PrincipalType.USER,
647
+ role_definition_id=administrator_role.id,
648
+ scope=vault.id,
649
+ )
650
+
436
651
  return vault
437
652
 
438
- def _add_webapp_secret(self, vault: azure.keyvault.Vault, secret_name: str, config_secret_name: str, suffix: str):
653
+ def _add_webapp_secret(
654
+ self,
655
+ vault: azure.keyvault.Vault,
656
+ secret_name: str,
657
+ config_secret_name: str,
658
+ suffix: str,
659
+ ):
439
660
  secret = self._config.require_secret(config_secret_name)
440
661
 
441
662
  # Normalize the secret name
@@ -495,13 +716,13 @@ class DjangoDeployment(pulumi.ComponentResource):
495
716
  db_name: str,
496
717
  repository_url: str,
497
718
  repository_branch: str,
498
- website_hosts: list[str],
719
+ website_hosts: list[HostDefinition],
499
720
  django_settings_module: str,
500
- environment_variables: dict[str, str] = {},
501
- secrets: dict[str, str] = {},
721
+ environment_variables: Optional[dict[str, str]] = None,
722
+ secrets: Optional[dict[str, str]] = None,
502
723
  comms_data_location: Optional[str] = None,
503
- comms_domains: Optional[list[str]] = [],
504
- vault_administrators: Optional[list[str]] = [],
724
+ comms_domains: Optional[list[HostDefinition]] = None,
725
+ vault_administrators: Optional[list[str]] = None,
505
726
  ) -> azure.web.WebApp:
506
727
  """
507
728
  Create a Django website with it's own database and storage containers.
@@ -549,7 +770,11 @@ class DjangoDeployment(pulumi.ComponentResource):
549
770
 
550
771
  # Communication Services (optional)
551
772
  if comms_data_location:
773
+ if not comms_domains:
774
+ comms_domains = []
775
+
552
776
  comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
777
+
553
778
  # Add the service endpoint as environment variable
554
779
  environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
555
780
  else:
@@ -575,6 +800,8 @@ class DjangoDeployment(pulumi.ComponentResource):
575
800
  for key, value in environment_variables.items()
576
801
  ]
577
802
 
803
+ allowed_hosts = pulumi.Output.concat(*[pulumi.Output.concat(host.full_host, ",") for host in website_hosts])
804
+
578
805
  app = azure.web.WebApp(
579
806
  f"app-{name}-{self._name}",
580
807
  resource_group_name=self._rg,
@@ -603,12 +830,18 @@ class DjangoDeployment(pulumi.ComponentResource):
603
830
  # azure.web.NameValuePairArgs(name="DEBUG", value="true"),
604
831
  azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
605
832
  azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
606
- azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=",".join(website_hosts)),
833
+ azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=allowed_hosts),
607
834
  # Vault settings
608
835
  azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
609
836
  # Storage settings
610
- azure.web.NameValuePairArgs(name="AZURE_STORAGE_ACCOUNT_NAME", value=self._storage_account.name),
611
- azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_STATICFILES", value=static_container.name),
837
+ azure.web.NameValuePairArgs(
838
+ name="AZURE_STORAGE_ACCOUNT_NAME",
839
+ value=self._storage_account.name,
840
+ ),
841
+ azure.web.NameValuePairArgs(
842
+ name="AZURE_STORAGE_CONTAINER_STATICFILES",
843
+ value=static_container.name,
844
+ ),
612
845
  azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
613
846
  # CDN
614
847
  azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
@@ -631,9 +864,85 @@ class DjangoDeployment(pulumi.ComponentResource):
631
864
  # We need this to verify custom domains
632
865
  pulumi.export(f"{name}_site_domain_verification_id", app.custom_domain_verification_id)
633
866
  pulumi.export(f"{name}_site_domain_cname", app.default_host_name)
867
+ virtual_ip = app.outbound_ip_addresses.apply(lambda addresses: addresses.split(",")[-1])
868
+ pulumi.export(f"{name}_site_virtual_ip", virtual_ip)
869
+
870
+ # Get the URL of the publish profile.
871
+ # Use app.identity here too to ensure the app is actually created before getting credentials.
872
+ credentials = pulumi.Output.all(self._rg, app.name, app.identity).apply(
873
+ lambda args: azure.web.list_web_app_publishing_credentials(
874
+ resource_group_name=args[0],
875
+ name=args[1],
876
+ )
877
+ )
878
+
879
+ pulumi.export(f"{name}_deploy_url", pulumi.Output.concat(credentials.scm_uri, "/deploy"))
634
880
 
635
881
  for host in website_hosts:
636
- self._add_webapp_host(app=app, host=host, suffix=f"{name}-{self._name}")
882
+ dependencies = []
883
+
884
+ if host.zone:
885
+ # Create a DNS record in the zone
886
+
887
+ if host.host == "@":
888
+ # Create a A record for the virtual IP address
889
+ a = azure.network.RecordSet(
890
+ f"dns-a-{name}-{self._name}-{host.identifier}",
891
+ resource_group_name=self._rg,
892
+ zone_name=host.zone.name,
893
+ relative_record_set_name=host.host,
894
+ record_type="A",
895
+ ttl=3600,
896
+ a_records=[
897
+ azure.network.ARecordArgs(
898
+ ipv4_address=virtual_ip,
899
+ )
900
+ ],
901
+ )
902
+
903
+ dependencies.append(a)
904
+ else:
905
+ # Create a CNAME record for the custom hostname
906
+ cname = azure.network.RecordSet(
907
+ f"dns-cname-{name}-{self._name}-{host.identifier}",
908
+ resource_group_name=self._rg,
909
+ zone_name=host.zone.name,
910
+ relative_record_set_name=host.host,
911
+ record_type="CNAME",
912
+ ttl=3600,
913
+ cname_record=azure.network.CnameRecordArgs(
914
+ cname=app.default_host_name,
915
+ ),
916
+ )
917
+ dependencies.append(cname)
918
+
919
+ # For the certificate validation to work
920
+ relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
921
+
922
+ txt_validation = azure.network.RecordSet(
923
+ f"dns-txt-{name}-{self._name}-{host.identifier}",
924
+ resource_group_name=self._rg,
925
+ zone_name=host.zone.name,
926
+ relative_record_set_name=relative_record_set_name,
927
+ record_type="TXT",
928
+ ttl=3600,
929
+ txt_records=[
930
+ azure.network.TxtRecordArgs(
931
+ value=[app.custom_domain_verification_id],
932
+ )
933
+ ],
934
+ )
935
+
936
+ dependencies.append(txt_validation)
937
+
938
+ # Add the host with optional dependencies
939
+ self._add_webapp_host(
940
+ app=app,
941
+ host=host.full_host,
942
+ suffix=f"{name}-{self._name}",
943
+ identifier=host.identifier,
944
+ depends_on=dependencies,
945
+ )
637
946
 
638
947
  # To enable deployment from GitLab
639
948
  azure.web.WebAppSourceControl(
@@ -649,7 +958,8 @@ class DjangoDeployment(pulumi.ComponentResource):
649
958
 
650
959
  # Where we can retrieve the SSH key
651
960
  pulumi.export(
652
- f"{name}_deploy_ssh_key_url", app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1")
961
+ f"{name}_deploy_ssh_key_url",
962
+ app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1"),
653
963
  )
654
964
 
655
965
  # Find the role for Key Vault Secrets User
@@ -722,10 +1032,7 @@ class DjangoDeployment(pulumi.ComponentResource):
722
1032
  )
723
1033
 
724
1034
  # Create a CORS rules for this website
725
- if website_hosts:
726
- origins = [f"https://{host}" for host in website_hosts]
727
- else:
728
- origins = ["*"]
1035
+ origins = [pulumi.Output.concat("https://", host.full_host) for host in website_hosts] if website_hosts else ["*"]
729
1036
 
730
1037
  azure.storage.BlobServiceProperties(
731
1038
  f"sa-{name}-blob-properties",
@@ -746,3 +1053,12 @@ class DjangoDeployment(pulumi.ComponentResource):
746
1053
  )
747
1054
 
748
1055
  return app
1056
+
1057
+ def _strip_off_dns_zone_name(self, host: str, zone: azure.network.Zone) -> pulumi.Output[str]:
1058
+ """
1059
+ Strip off the DNS zone name from the host name.
1060
+
1061
+ :param host: The host name
1062
+ :return: The host name without the DNS zone
1063
+ """
1064
+ return zone.name.apply(lambda name: host[: -len(name) - 1])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pulumi-django-azure
3
- Version: 1.0.12
3
+ Version: 1.0.14
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
@@ -1 +0,0 @@
1
- from .django_deployment import DjangoDeployment # noqa: F401