pulumi-django-azure 1.0.12__py3-none-any.whl → 1.0.15__py3-none-any.whl

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 +1 @@
1
- from .django_deployment import DjangoDeployment # noqa: F401
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,10 @@ 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
+ storage_allowed_origins: Optional[Sequence[str]] = None,
69
+ pgadmin_access_ip: Optional[Sequence[str]] = None,
70
+ pgadmin_dns_zone: Optional[azure.network.Zone] = None,
71
+ cdn_host: Optional[HostDefinition] = None,
23
72
  opts=None,
24
73
  ):
25
74
  """
@@ -34,7 +83,10 @@ class DjangoDeployment(pulumi.ComponentResource):
34
83
  :param appservice_ip_prefix: The IP prefix for the app service subnet.
35
84
  :param app_service_sku: The SKU for the app service plan.
36
85
  :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)
86
+ :param storage_allowed_origins: The origins (hosts) to allow access through CORS policy. You can specify '*' to allow all.
87
+ :param pgadmin_access_ip: The IP addresses to allow access to pgAdmin. If empty, all IP addresses are allowed.
88
+ :param pgadmin_dns_zone: The Azure DNS zone to a pgadmin DNS record in. (optional)
89
+ :param cdn_host: A custom CDN host name. (optional)
38
90
  :param opts: The resource options
39
91
  """
40
92
 
@@ -49,7 +101,7 @@ class DjangoDeployment(pulumi.ComponentResource):
49
101
  self._vnet = vnet
50
102
 
51
103
  # Storage resources
52
- self._create_storage(account_name=storage_account_name)
104
+ self._create_storage(account_name=storage_account_name, allowed_origins=storage_allowed_origins)
53
105
  self._cdn_host = self._create_cdn(custom_host=cdn_host)
54
106
 
55
107
  # PostgreSQL resources
@@ -67,9 +119,9 @@ class DjangoDeployment(pulumi.ComponentResource):
67
119
  self._app_service_plan = self._create_app_service_plan(sku=app_service_sku)
68
120
 
69
121
  # Create a pgAdmin app
70
- self._create_pgadmin_app()
122
+ self._create_pgadmin_app(access_ip=pgadmin_access_ip, dns_zone=pgadmin_dns_zone)
71
123
 
72
- def _create_storage(self, account_name: str):
124
+ def _create_storage(self, account_name: str, allowed_origins: Optional[Sequence[str]] = None):
73
125
  # Create blob storage
74
126
  self._storage_account = azure.storage.StorageAccount(
75
127
  f"sa-{self._name}",
@@ -85,7 +137,26 @@ class DjangoDeployment(pulumi.ComponentResource):
85
137
  enable_https_traffic_only=True,
86
138
  )
87
139
 
88
- def _create_cdn(self, custom_host: Optional[str]) -> pulumi.Output[str]:
140
+ if allowed_origins:
141
+ azure.storage.BlobServiceProperties(
142
+ f"sa-{self._name}-blob-properties",
143
+ resource_group_name=self._rg,
144
+ account_name=self._storage_account.name,
145
+ blob_services_name="default",
146
+ cors=azure.storage.CorsRulesArgs(
147
+ cors_rules=[
148
+ azure.storage.CorsRuleArgs(
149
+ allowed_headers=["*"],
150
+ allowed_methods=["GET", "OPTIONS", "HEAD"],
151
+ allowed_origins=allowed_origins,
152
+ exposed_headers=["Access-Control-Allow-Origin"],
153
+ max_age_in_seconds=86400,
154
+ )
155
+ ]
156
+ ),
157
+ )
158
+
159
+ def _create_cdn(self, custom_host: Optional[HostDefinition]) -> pulumi.Output[str]:
89
160
  """
90
161
  Create a CDN endpoint. If a host name is given, it will be used as the custom domain.
91
162
  Otherwise, the default CDN host name will be returned.
@@ -128,22 +199,48 @@ class DjangoDeployment(pulumi.ComponentResource):
128
199
  profile_name=self._cdn_profile.name,
129
200
  origin_host_header=endpoint_origin,
130
201
  origins=[azure.cdn.DeepCreatedOriginArgs(name="origin-storage", host_name=endpoint_origin)],
131
- query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.IGNORE_QUERY_STRING,
202
+ query_string_caching_behavior=azure.cdn.QueryStringCachingBehavior.USE_QUERY_STRING,
132
203
  )
133
204
 
134
205
  pulumi.export("cdn_cname", self._cdn_endpoint.host_name)
135
206
 
136
207
  # Add custom domain if given
137
208
  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
- )
209
+ if custom_host.zone:
210
+ # Create a DNS record for the custom host in the given zone
211
+ rs = azure.network.RecordSet(
212
+ f"cdn-cname-{self._name}",
213
+ resource_group_name=self._rg,
214
+ zone_name=custom_host.zone.name,
215
+ relative_record_set_name=custom_host.host,
216
+ record_type="CNAME",
217
+ ttl=3600,
218
+ target_resource=azure.network.SubResourceArgs(
219
+ id=self._cdn_endpoint.id,
220
+ ),
221
+ )
222
+
223
+ azure.cdn.CustomDomain(
224
+ f"cdn-custom-domain-{self._name}",
225
+ resource_group_name=self._rg,
226
+ profile_name=self._cdn_profile.name,
227
+ endpoint_name=self._cdn_endpoint.name,
228
+ host_name=custom_host.full_host,
229
+ opts=pulumi.ResourceOptions(depends_on=rs),
230
+ )
231
+
232
+ return custom_host.full_host
233
+ else:
234
+ # Add custom hostname without a zone
235
+ azure.cdn.CustomDomain(
236
+ f"cdn-custom-domain-{self._name}",
237
+ resource_group_name=self._rg,
238
+ profile_name=self._cdn_profile.name,
239
+ endpoint_name=self._cdn_endpoint.name,
240
+ host_name=custom_host.host,
241
+ )
145
242
 
146
- return custom_host
243
+ return custom_host.host
147
244
  else:
148
245
  # Return the default CDN host name
149
246
  return self._cdn_endpoint.host_name
@@ -200,7 +297,11 @@ class DjangoDeployment(pulumi.ComponentResource):
200
297
  pulumi.export("pgsql_host", self._pgsql.fully_qualified_domain_name)
201
298
 
202
299
  def _create_subnet(
203
- self, name, prefix, delegation_service: Optional[str] = None, service_endpoints: Sequence[str] = []
300
+ self,
301
+ name,
302
+ prefix,
303
+ delegation_service: Optional[str] = None,
304
+ service_endpoints: Sequence[str] = [],
204
305
  ) -> azure.network.Subnet:
205
306
  """
206
307
  Generic method to create a subnet with a delegation.
@@ -239,7 +340,22 @@ class DjangoDeployment(pulumi.ComponentResource):
239
340
  sku=sku,
240
341
  )
241
342
 
242
- def _create_pgadmin_app(self):
343
+ def _create_pgadmin_app(self, access_ip: Optional[Sequence[str]] = None, dns_zone: Optional[azure.network.Zone] = None):
344
+ # Determine the IP restrictions
345
+ ip_restrictions = []
346
+ default_restriction = azure.web.DefaultAction.ALLOW
347
+ if access_ip:
348
+ default_restriction = azure.web.DefaultAction.DENY
349
+
350
+ for ip in access_ip:
351
+ ip_restrictions.append(
352
+ azure.web.IpSecurityRestrictionArgs(
353
+ action="Allow",
354
+ ip_address=ip,
355
+ priority=300,
356
+ )
357
+ )
358
+
243
359
  # The app itself
244
360
  app = azure.web.WebApp(
245
361
  f"app-pgadmin-{self._name}",
@@ -255,7 +371,10 @@ class DjangoDeployment(pulumi.ComponentResource):
255
371
  linux_fx_version="DOCKER|dpage/pgadmin4",
256
372
  health_check_path="/misc/ping",
257
373
  app_settings=[
258
- azure.web.NameValuePairArgs(name="DOCKER_REGISTRY_SERVER_URL", value="https://index.docker.io/v1"),
374
+ azure.web.NameValuePairArgs(
375
+ name="DOCKER_REGISTRY_SERVER_URL",
376
+ value="https://index.docker.io/v1",
377
+ ),
259
378
  azure.web.NameValuePairArgs(name="DOCKER_ENABLE_CI", value="true"),
260
379
  # azure.web.NameValuePairArgs(name="WEBSITE_HTTPLOGGING_RETENTION_DAYS", value="7"),
261
380
  # pgAdmin settings
@@ -263,6 +382,9 @@ class DjangoDeployment(pulumi.ComponentResource):
263
382
  azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_EMAIL", value="dbadmin@dbadmin.net"),
264
383
  azure.web.NameValuePairArgs(name="PGADMIN_DEFAULT_PASSWORD", value="dbadmin"),
265
384
  ],
385
+ # IP restrictions
386
+ ip_security_restrictions_default_action=default_restriction,
387
+ ip_security_restrictions=ip_restrictions,
266
388
  ),
267
389
  )
268
390
 
@@ -274,6 +396,50 @@ class DjangoDeployment(pulumi.ComponentResource):
274
396
  share_name="pgadmin",
275
397
  )
276
398
 
399
+ if dns_zone:
400
+ # Create a DNS record for the pgAdmin app
401
+ cname = azure.network.RecordSet(
402
+ f"dns-cname-pgadmin-{self._name}",
403
+ resource_group_name=self._rg,
404
+ zone_name=dns_zone.name,
405
+ relative_record_set_name="pgadmin",
406
+ record_type="CNAME",
407
+ ttl=3600,
408
+ cname_record=azure.network.CnameRecordArgs(
409
+ cname=app.default_host_name,
410
+ ),
411
+ )
412
+
413
+ # For the certificate validation to work
414
+ txt_validation = azure.network.RecordSet(
415
+ f"dns-txt-pgadmin-{self._name}",
416
+ resource_group_name=self._rg,
417
+ zone_name=dns_zone.name,
418
+ relative_record_set_name="asuid.pgadmin",
419
+ record_type="TXT",
420
+ ttl=3600,
421
+ txt_records=[
422
+ azure.network.TxtRecordArgs(
423
+ value=[app.custom_domain_verification_id],
424
+ )
425
+ ],
426
+ )
427
+
428
+ # Add custom hostname
429
+ self._add_webapp_host(
430
+ app=app,
431
+ host=dns_zone.name.apply(lambda name: f"pgadmin.{name}"),
432
+ suffix=self._name,
433
+ depends_on=[cname, txt_validation],
434
+ identifier="pgadmin",
435
+ )
436
+
437
+ # Export the custom hostname
438
+ pulumi.export("pgadmin_url", dns_zone.name.apply(lambda name: f"https://pgadmin.{name}"))
439
+ else:
440
+ # Export the default hostname
441
+ pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
442
+
277
443
  # Mount the storage container
278
444
  azure.web.WebAppAzureStorageAccounts(
279
445
  f"app-pgadmin-mount-{self._name}",
@@ -290,9 +456,24 @@ class DjangoDeployment(pulumi.ComponentResource):
290
456
  },
291
457
  )
292
458
 
293
- pulumi.export("pgadmin_url", app.default_host_name.apply(lambda host: f"https://{host}"))
459
+ def _get_existing_web_app_host_name_binding(self, resource_group_name: str, app_name: str, host_name: str):
460
+ try:
461
+ return azure.web.get_web_app_host_name_binding(
462
+ resource_group_name=resource_group_name,
463
+ name=app_name,
464
+ host_name=host_name,
465
+ )
466
+ except Exception:
467
+ return None
294
468
 
295
- def _add_webapp_host(self, app: azure.web.WebApp, host: str, suffix: str):
469
+ def _add_webapp_host(
470
+ self,
471
+ app: azure.web.WebApp,
472
+ host: Union[str, pulumi.Input[str]],
473
+ suffix: str,
474
+ identifier: str,
475
+ depends_on: Optional[Sequence[pulumi.Resource]] = None,
476
+ ):
296
477
  """
297
478
  Because of a circular dependency, we need to create the certificate and the binding in two steps.
298
479
  First we create a binding without a certificate,
@@ -305,48 +486,93 @@ class DjangoDeployment(pulumi.ComponentResource):
305
486
  :param app: The web app
306
487
  :param host: The host name
307
488
  :param suffix: A suffix to make the resource name unique
489
+ :param depend_on: The resource to depend on (optional)
308
490
  """
309
491
 
310
- safe_host = host.replace(".", "-")
492
+ if not depends_on:
493
+ depends_on = []
311
494
 
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,
495
+ # Retrieve the existing binding (None if it doesn't exist)
496
+ existing_binding = pulumi.Output.all(app.resource_group, app.name, host).apply(
497
+ lambda args: self._get_existing_web_app_host_name_binding(
498
+ resource_group_name=args[0],
499
+ app_name=args[1],
500
+ host_name=args[2],
318
501
  )
502
+ )
319
503
 
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
- )
504
+ # Create an inline function that we will invoke through the Output.apply lambda
505
+ def _create_binding_with_cert(existing_binding):
506
+ if existing_binding:
507
+ # Create a managed certificate
508
+ # This will work because the binding exists actually
509
+ certificate = azure.web.Certificate(
510
+ f"cert-{suffix}-{identifier}",
511
+ resource_group_name=self._rg,
512
+ server_farm_id=app.server_farm_id,
513
+ canonical_name=host,
514
+ host_names=[host],
515
+ opts=pulumi.ResourceOptions(depends_on=depends_on),
516
+ )
329
517
 
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}",
518
+ # Create a new binding, replacing the old one,
519
+ # with the certificate
520
+ azure.web.WebAppHostNameBinding(
521
+ f"host-binding-{suffix}-{identifier}",
522
+ resource_group_name=self._rg,
523
+ name=app.name,
524
+ host_name=host,
525
+ ssl_state=azure.web.SslState.SNI_ENABLED,
526
+ thumbprint=certificate.thumbprint,
527
+ opts=pulumi.ResourceOptions(depends_on=depends_on),
528
+ )
529
+
530
+ else:
531
+ # Create a binding without a certificate
532
+ azure.web.WebAppHostNameBinding(
533
+ f"host-binding-{suffix}-{identifier}",
534
+ resource_group_name=self._rg,
535
+ name=app.name,
536
+ host_name=host,
537
+ opts=pulumi.ResourceOptions(depends_on=depends_on),
538
+ )
539
+
540
+ existing_binding.apply(_create_binding_with_cert)
541
+
542
+ def _create_comms_dns_records(self, suffix, host: HostDefinition, records: dict) -> list[azure.network.RecordSet]:
543
+ created_records = []
544
+
545
+ # Domain validation and SPF record (one TXT record with multiple values)
546
+ r = azure.network.RecordSet(
547
+ f"dns-comms-{suffix}-{host.identifier}-domain",
548
+ resource_group_name=self._rg,
549
+ zone_name=host.zone.name,
550
+ relative_record_set_name="@",
551
+ record_type="TXT",
552
+ ttl=3600,
553
+ txt_records=[
554
+ azure.network.TxtRecordArgs(value=[records["domain"]["value"]]),
555
+ azure.network.TxtRecordArgs(value=[records["s_pf"]["value"]]),
556
+ ],
557
+ )
558
+ created_records.append(r)
559
+
560
+ # DKIM records (two CNAME records)
561
+ for record in ("d_kim", "d_kim2"):
562
+ r = azure.network.RecordSet(
563
+ f"dns-comms-{suffix}-{host.identifier}-{record}",
344
564
  resource_group_name=self._rg,
345
- name=app.name,
346
- host_name=host,
565
+ zone_name=host.zone.name,
566
+ relative_record_set_name=records[record]["name"],
567
+ record_type="CNAME",
568
+ ttl=records[record]["ttl"],
569
+ cname_record=azure.network.CnameRecordArgs(cname=records[record]["value"]),
347
570
  )
571
+ created_records.append(r)
572
+
573
+ return created_records
348
574
 
349
- def _add_webapp_comms(self, data_location: str, domains: list[str], suffix: str) -> azure.communication.CommunicationService:
575
+ def _add_webapp_comms(self, data_location: str, domains: list[HostDefinition], suffix: str) -> azure.communication.CommunicationService:
350
576
  email_service = azure.communication.EmailService(
351
577
  f"comms-email-{suffix}",
352
578
  resource_group_name=self._rg,
@@ -355,30 +581,37 @@ class DjangoDeployment(pulumi.ComponentResource):
355
581
  )
356
582
 
357
583
  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
584
+ comm_dependencies = []
585
+
586
+ # Add our own custom domains
587
+ for domain in domains:
373
588
  d = azure.communication.Domain(
374
- f"comms-email-domain-{suffix}-azure",
589
+ f"comms-email-domain-{suffix}-{domain.identifier}",
375
590
  resource_group_name=self._rg,
376
591
  location="global",
377
- domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
378
- domain_name="AzureManagedDomain",
592
+ domain_management=azure.communication.DomainManagement.CUSTOMER_MANAGED,
593
+ domain_name=domain.full_host,
379
594
  email_service_name=email_service.name,
380
595
  )
381
- domain_resources.append(d.id.apply(lambda n: n))
596
+
597
+ if domain.zone:
598
+ # Create DNS records in the managed zone
599
+ comm_dependencies = pulumi.Output.all(suffix, domain, d.verification_records).apply(
600
+ lambda args: self._create_comms_dns_records(suffix=args[0], host=args[1], records=args[2])
601
+ )
602
+
603
+ domain_resources.append(d.id)
604
+
605
+ # Add an Azure managed domain
606
+ d = azure.communication.Domain(
607
+ f"comms-email-domain-{suffix}-azure",
608
+ resource_group_name=self._rg,
609
+ location="global",
610
+ domain_management=azure.communication.DomainManagement.AZURE_MANAGED,
611
+ domain_name="AzureManagedDomain",
612
+ email_service_name=email_service.name,
613
+ )
614
+ domain_resources.append(d.id)
382
615
 
383
616
  # Create Communication Services and link the domains
384
617
  comm_service = azure.communication.CommunicationService(
@@ -387,6 +620,7 @@ class DjangoDeployment(pulumi.ComponentResource):
387
620
  location="global",
388
621
  data_location=data_location,
389
622
  linked_domains=domain_resources,
623
+ opts=pulumi.ResourceOptions(depends_on=comm_dependencies),
390
624
  )
391
625
 
392
626
  return comm_service
@@ -415,27 +649,35 @@ class DjangoDeployment(pulumi.ComponentResource):
415
649
  ),
416
650
  )
417
651
 
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
652
  # 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,
653
+ if administrators:
654
+ # Find the Key Vault Administrator role
655
+ administrator_role = vault.id.apply(
656
+ lambda scope: azure.authorization.get_role_definition(
657
+ role_definition_id="00482a5a-887f-4fb3-b363-3b7fe8e74483",
658
+ scope=scope,
659
+ )
434
660
  )
435
661
 
662
+ # Actual administrator roles
663
+ for a in administrators:
664
+ azure.authorization.RoleAssignment(
665
+ f"ra-{suffix}-vault-admin-{a}",
666
+ principal_id=a,
667
+ principal_type=azure.authorization.PrincipalType.USER,
668
+ role_definition_id=administrator_role.id,
669
+ scope=vault.id,
670
+ )
671
+
436
672
  return vault
437
673
 
438
- def _add_webapp_secret(self, vault: azure.keyvault.Vault, secret_name: str, config_secret_name: str, suffix: str):
674
+ def _add_webapp_secret(
675
+ self,
676
+ vault: azure.keyvault.Vault,
677
+ secret_name: str,
678
+ config_secret_name: str,
679
+ suffix: str,
680
+ ):
439
681
  secret = self._config.require_secret(config_secret_name)
440
682
 
441
683
  # Normalize the secret name
@@ -495,13 +737,13 @@ class DjangoDeployment(pulumi.ComponentResource):
495
737
  db_name: str,
496
738
  repository_url: str,
497
739
  repository_branch: str,
498
- website_hosts: list[str],
740
+ website_hosts: list[HostDefinition],
499
741
  django_settings_module: str,
500
- environment_variables: dict[str, str] = {},
501
- secrets: dict[str, str] = {},
742
+ environment_variables: Optional[dict[str, str]] = None,
743
+ secrets: Optional[dict[str, str]] = None,
502
744
  comms_data_location: Optional[str] = None,
503
- comms_domains: Optional[list[str]] = [],
504
- vault_administrators: Optional[list[str]] = [],
745
+ comms_domains: Optional[list[HostDefinition]] = None,
746
+ vault_administrators: Optional[list[str]] = None,
505
747
  ) -> azure.web.WebApp:
506
748
  """
507
749
  Create a Django website with it's own database and storage containers.
@@ -549,7 +791,11 @@ class DjangoDeployment(pulumi.ComponentResource):
549
791
 
550
792
  # Communication Services (optional)
551
793
  if comms_data_location:
794
+ if not comms_domains:
795
+ comms_domains = []
796
+
552
797
  comms = self._add_webapp_comms(comms_data_location, comms_domains, f"{name}-{self._name}")
798
+
553
799
  # Add the service endpoint as environment variable
554
800
  environment_variables["AZURE_COMMUNICATION_SERVICE_ENDPOINT"] = comms.host_name.apply(lambda host: f"https://{host}")
555
801
  else:
@@ -575,6 +821,8 @@ class DjangoDeployment(pulumi.ComponentResource):
575
821
  for key, value in environment_variables.items()
576
822
  ]
577
823
 
824
+ allowed_hosts = pulumi.Output.concat(*[pulumi.Output.concat(host.full_host, ",") for host in website_hosts])
825
+
578
826
  app = azure.web.WebApp(
579
827
  f"app-{name}-{self._name}",
580
828
  resource_group_name=self._rg,
@@ -603,12 +851,18 @@ class DjangoDeployment(pulumi.ComponentResource):
603
851
  # azure.web.NameValuePairArgs(name="DEBUG", value="true"),
604
852
  azure.web.NameValuePairArgs(name="DJANGO_SETTINGS_MODULE", value=django_settings_module),
605
853
  azure.web.NameValuePairArgs(name="DJANGO_SECRET_KEY", value=secret_key.result),
606
- azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=",".join(website_hosts)),
854
+ azure.web.NameValuePairArgs(name="DJANGO_ALLOWED_HOSTS", value=allowed_hosts),
607
855
  # Vault settings
608
856
  azure.web.NameValuePairArgs(name="AZURE_KEY_VAULT", value=vault.name),
609
857
  # 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),
858
+ azure.web.NameValuePairArgs(
859
+ name="AZURE_STORAGE_ACCOUNT_NAME",
860
+ value=self._storage_account.name,
861
+ ),
862
+ azure.web.NameValuePairArgs(
863
+ name="AZURE_STORAGE_CONTAINER_STATICFILES",
864
+ value=static_container.name,
865
+ ),
612
866
  azure.web.NameValuePairArgs(name="AZURE_STORAGE_CONTAINER_MEDIA", value=media_container.name),
613
867
  # CDN
614
868
  azure.web.NameValuePairArgs(name="CDN_HOST", value=self._cdn_host),
@@ -631,9 +885,85 @@ class DjangoDeployment(pulumi.ComponentResource):
631
885
  # We need this to verify custom domains
632
886
  pulumi.export(f"{name}_site_domain_verification_id", app.custom_domain_verification_id)
633
887
  pulumi.export(f"{name}_site_domain_cname", app.default_host_name)
888
+ virtual_ip = app.outbound_ip_addresses.apply(lambda addresses: addresses.split(",")[-1])
889
+ pulumi.export(f"{name}_site_virtual_ip", virtual_ip)
890
+
891
+ # Get the URL of the publish profile.
892
+ # Use app.identity here too to ensure the app is actually created before getting credentials.
893
+ credentials = pulumi.Output.all(self._rg, app.name, app.identity).apply(
894
+ lambda args: azure.web.list_web_app_publishing_credentials(
895
+ resource_group_name=args[0],
896
+ name=args[1],
897
+ )
898
+ )
899
+
900
+ pulumi.export(f"{name}_deploy_url", pulumi.Output.concat(credentials.scm_uri, "/deploy"))
634
901
 
635
902
  for host in website_hosts:
636
- self._add_webapp_host(app=app, host=host, suffix=f"{name}-{self._name}")
903
+ dependencies = []
904
+
905
+ if host.zone:
906
+ # Create a DNS record in the zone
907
+
908
+ if host.host == "@":
909
+ # Create a A record for the virtual IP address
910
+ a = azure.network.RecordSet(
911
+ f"dns-a-{name}-{self._name}-{host.identifier}",
912
+ resource_group_name=self._rg,
913
+ zone_name=host.zone.name,
914
+ relative_record_set_name=host.host,
915
+ record_type="A",
916
+ ttl=3600,
917
+ a_records=[
918
+ azure.network.ARecordArgs(
919
+ ipv4_address=virtual_ip,
920
+ )
921
+ ],
922
+ )
923
+
924
+ dependencies.append(a)
925
+ else:
926
+ # Create a CNAME record for the custom hostname
927
+ cname = azure.network.RecordSet(
928
+ f"dns-cname-{name}-{self._name}-{host.identifier}",
929
+ resource_group_name=self._rg,
930
+ zone_name=host.zone.name,
931
+ relative_record_set_name=host.host,
932
+ record_type="CNAME",
933
+ ttl=3600,
934
+ cname_record=azure.network.CnameRecordArgs(
935
+ cname=app.default_host_name,
936
+ ),
937
+ )
938
+ dependencies.append(cname)
939
+
940
+ # For the certificate validation to work
941
+ relative_record_set_name = "asuid" if host.host == "@" else pulumi.Output.concat("asuid.", host.host)
942
+
943
+ txt_validation = azure.network.RecordSet(
944
+ f"dns-txt-{name}-{self._name}-{host.identifier}",
945
+ resource_group_name=self._rg,
946
+ zone_name=host.zone.name,
947
+ relative_record_set_name=relative_record_set_name,
948
+ record_type="TXT",
949
+ ttl=3600,
950
+ txt_records=[
951
+ azure.network.TxtRecordArgs(
952
+ value=[app.custom_domain_verification_id],
953
+ )
954
+ ],
955
+ )
956
+
957
+ dependencies.append(txt_validation)
958
+
959
+ # Add the host with optional dependencies
960
+ self._add_webapp_host(
961
+ app=app,
962
+ host=host.full_host,
963
+ suffix=f"{name}-{self._name}",
964
+ identifier=host.identifier,
965
+ depends_on=dependencies,
966
+ )
637
967
 
638
968
  # To enable deployment from GitLab
639
969
  azure.web.WebAppSourceControl(
@@ -649,7 +979,8 @@ class DjangoDeployment(pulumi.ComponentResource):
649
979
 
650
980
  # Where we can retrieve the SSH key
651
981
  pulumi.export(
652
- f"{name}_deploy_ssh_key_url", app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1")
982
+ f"{name}_deploy_ssh_key_url",
983
+ app.name.apply(lambda name: f"https://{name}.scm.azurewebsites.net/api/sshkey?ensurePublicKey=1"),
653
984
  )
654
985
 
655
986
  # Find the role for Key Vault Secrets User
@@ -721,28 +1052,13 @@ class DjangoDeployment(pulumi.ComponentResource):
721
1052
  scope=self._cdn_endpoint.id,
722
1053
  )
723
1054
 
724
- # 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 = ["*"]
1055
+ return app
729
1056
 
730
- azure.storage.BlobServiceProperties(
731
- f"sa-{name}-blob-properties",
732
- resource_group_name=self._rg,
733
- account_name=self._storage_account.name,
734
- blob_services_name="default",
735
- cors=azure.storage.CorsRulesArgs(
736
- cors_rules=[
737
- azure.storage.CorsRuleArgs(
738
- allowed_headers=["*"],
739
- allowed_methods=["GET", "OPTIONS", "HEAD"],
740
- allowed_origins=origins,
741
- exposed_headers=["Access-Control-Allow-Origin"],
742
- max_age_in_seconds=86400,
743
- )
744
- ]
745
- ),
746
- )
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.
747
1060
 
748
- return app
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])
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
@@ -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.15
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
@@ -33,9 +33,9 @@ Classifier: Programming Language :: Python :: 3
33
33
  Requires-Python: >=3.9
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: pulumi >=3.99.0
37
- Requires-Dist: pulumi-azure-native >=2.24.0
38
- Requires-Dist: pulumi-random >=4.14.0
36
+ Requires-Dist: pulumi>=3.99.0
37
+ Requires-Dist: pulumi-azure-native>=2.24.0
38
+ Requires-Dist: pulumi-random>=4.14.0
39
39
 
40
40
  # Pulumi Django Deployment
41
41
 
@@ -0,0 +1,7 @@
1
+ pulumi_django_azure/__init__.py,sha256=5RY9reSVNw-HULrOXfhcq3cyPne-94ojFmeV1m6kIVg,79
2
+ pulumi_django_azure/django_deployment.py,sha256=dTbzASxVeDSLzTx3MN5UcbZTVhtZBbsaplF-mrkOvoc,44421
3
+ pulumi_django_azure-1.0.15.dist-info/LICENSE,sha256=tlZQiilfsHDYlvhWMA5PvDV2FxpaCQbE9aapcygnhEQ,1088
4
+ pulumi_django_azure-1.0.15.dist-info/METADATA,sha256=FUubwm450nWLBkxdvinFLDu-jTH8G8POYpcDq_WcD3g,13954
5
+ pulumi_django_azure-1.0.15.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
6
+ pulumi_django_azure-1.0.15.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
7
+ pulumi_django_azure-1.0.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,7 +0,0 @@
1
- pulumi_django_azure/__init__.py,sha256=tXTvPfr8-Nll5cjMyY9yj_0z_PQ0XAcxihzHRCES-hU,63
2
- pulumi_django_azure/django_deployment.py,sha256=MBdNH2iEOoNyWY5mxiLy8iUkW514LsG-wNjo4MVZlqY,31479
3
- pulumi_django_azure-1.0.12.dist-info/LICENSE,sha256=NX2LN3U319Zaac8b7ZgfNOco_nTBbN531X_M_13niSg,1087
4
- pulumi_django_azure-1.0.12.dist-info/METADATA,sha256=hPKNy_NeqAhvQGJFn-4Zw8AUiWU6pM0jpS9ypjzX2as,13957
5
- pulumi_django_azure-1.0.12.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
- pulumi_django_azure-1.0.12.dist-info/top_level.txt,sha256=MNPRJhq-_G8EMCHRkjdcb_xrqzOkmKogXUGV7Ysz3g0,20
7
- pulumi_django_azure-1.0.12.dist-info/RECORD,,